groundmail-cli 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/dist/cli.js +1695 -1308
- package/package.json +6 -3
package/dist/cli.js
CHANGED
|
@@ -1264,625 +1264,1098 @@ async function runListKeysCommandWithClient(args, env = process.env) {
|
|
|
1264
1264
|
}
|
|
1265
1265
|
}
|
|
1266
1266
|
|
|
1267
|
-
// src/
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1267
|
+
// src/session/brand.ts
|
|
1268
|
+
var GROUNDMAIL = {
|
|
1269
|
+
bin: "groundmail",
|
|
1270
|
+
product: "Groundmail",
|
|
1271
|
+
configDirName: "groundmail",
|
|
1272
|
+
keyEnvVar: "GROUNDMAIL_API_KEY",
|
|
1273
|
+
npx: "npx -y groundmail-cli@latest"
|
|
1274
|
+
};
|
|
1275
|
+
function cliBrand(_env = process.env) {
|
|
1276
|
+
return GROUNDMAIL;
|
|
1272
1277
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
1278
|
-
}
|
|
1279
|
-
const name = readFlag16(args, "--name");
|
|
1280
|
-
const scope = readFlag16(args, "--scope");
|
|
1281
|
-
if (scope && scope !== "full" && scope !== "sending") {
|
|
1282
|
-
return { exitCode: 1, error: "--scope must be full or sending" };
|
|
1283
|
-
}
|
|
1284
|
-
const { Groundmail } = await import("groundmail");
|
|
1285
|
-
const client = new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
|
|
1286
|
-
try {
|
|
1287
|
-
const result = await client.apiKeys.create({
|
|
1288
|
-
...name ? { name } : {},
|
|
1289
|
-
...scope ? { scope } : {}
|
|
1290
|
-
});
|
|
1291
|
-
console.log(JSON.stringify(result));
|
|
1292
|
-
return { exitCode: 0 };
|
|
1293
|
-
} catch (error) {
|
|
1294
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1295
|
-
console.error(message);
|
|
1296
|
-
return { exitCode: 1, error: message };
|
|
1297
|
-
}
|
|
1278
|
+
var SDK_DEFAULT_API_URL = "https://api.groundmail.dev";
|
|
1279
|
+
var DEFAULT_API_URL_ENV = "GROUNDMAIL_DEFAULT_API_URL";
|
|
1280
|
+
function defaultApiUrl(env = process.env) {
|
|
1281
|
+
return trimUrl(env[DEFAULT_API_URL_ENV]) || SDK_DEFAULT_API_URL;
|
|
1298
1282
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
function readFlag17(args, flag) {
|
|
1302
|
-
const index = args.indexOf(flag);
|
|
1303
|
-
if (index === -1) return void 0;
|
|
1304
|
-
return args[index + 1];
|
|
1283
|
+
function explicitApiUrl(env = process.env) {
|
|
1284
|
+
return trimUrl(env.GROUNDMAIL_API_URL) || null;
|
|
1305
1285
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const baseUrl = readFlag17(args, "--base-url");
|
|
1309
|
-
const id = readFlag17(args, "--id");
|
|
1310
|
-
if (!apiKey) {
|
|
1311
|
-
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
1312
|
-
}
|
|
1313
|
-
if (!id) {
|
|
1314
|
-
return { exitCode: 1, error: "Required: --id <api-key-id>" };
|
|
1315
|
-
}
|
|
1316
|
-
const { Groundmail } = await import("groundmail");
|
|
1317
|
-
const client = new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
|
|
1318
|
-
try {
|
|
1319
|
-
const result = await client.apiKeys.remove(id);
|
|
1320
|
-
console.log(JSON.stringify(result));
|
|
1321
|
-
return { exitCode: 0 };
|
|
1322
|
-
} catch (error) {
|
|
1323
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1324
|
-
console.error(message);
|
|
1325
|
-
return { exitCode: 1, error: message };
|
|
1326
|
-
}
|
|
1286
|
+
function trimUrl(value) {
|
|
1287
|
+
return (value ?? "").trim().replace(/\/+$/, "");
|
|
1327
1288
|
}
|
|
1328
1289
|
|
|
1329
|
-
//
|
|
1330
|
-
|
|
1331
|
-
const index = args.indexOf(flag);
|
|
1332
|
-
if (index === -1) return void 0;
|
|
1333
|
-
return args[index + 1];
|
|
1334
|
-
}
|
|
1335
|
-
function parseSuppressionsCommonArgs(args) {
|
|
1336
|
-
return {
|
|
1337
|
-
apiKey: readFlag18(args, "--api-key"),
|
|
1338
|
-
baseUrl: readFlag18(args, "--base-url")
|
|
1339
|
-
};
|
|
1340
|
-
}
|
|
1341
|
-
function resolveApiKey6(options, env) {
|
|
1342
|
-
return options.apiKey || env.GROUNDMAIL_API_KEY || null;
|
|
1343
|
-
}
|
|
1344
|
-
async function createSuppressionsClient(apiKey, baseUrl) {
|
|
1345
|
-
const { Groundmail } = await import("groundmail");
|
|
1346
|
-
return new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
|
|
1347
|
-
}
|
|
1290
|
+
// package.json
|
|
1291
|
+
var version = "0.16.1";
|
|
1348
1292
|
|
|
1349
|
-
// src/
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1293
|
+
// src/user-agent.ts
|
|
1294
|
+
function buildUserAgent(own) {
|
|
1295
|
+
return `${own.name}/${own.version} node/${process.versions.node}`;
|
|
1296
|
+
}
|
|
1297
|
+
function withUserAgent(init, userAgent) {
|
|
1298
|
+
const headers = init?.headers;
|
|
1299
|
+
if (headers instanceof Headers) {
|
|
1300
|
+
const next = new Headers(headers);
|
|
1301
|
+
if (!next.has("user-agent")) next.set("User-Agent", userAgent);
|
|
1302
|
+
return { ...init, headers: next };
|
|
1355
1303
|
}
|
|
1356
|
-
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
const cursor = readFlag18(args, "--cursor");
|
|
1360
|
-
const email = readFlag18(args, "--email");
|
|
1361
|
-
const result = await client.suppressions.list({
|
|
1362
|
-
...limitRaw ? { limit: Number(limitRaw) } : {},
|
|
1363
|
-
...cursor ? { cursor } : {},
|
|
1364
|
-
...email ? { email } : {}
|
|
1365
|
-
});
|
|
1366
|
-
console.log(JSON.stringify(result));
|
|
1367
|
-
return { exitCode: 0 };
|
|
1368
|
-
} catch (error) {
|
|
1369
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1304
|
+
if (Array.isArray(headers)) {
|
|
1305
|
+
const has2 = headers.some(([key]) => key.toLowerCase() === "user-agent");
|
|
1306
|
+
return { ...init, headers: has2 ? headers : [...headers, ["User-Agent", userAgent]] };
|
|
1370
1307
|
}
|
|
1308
|
+
const record = headers ?? {};
|
|
1309
|
+
const has = Object.keys(record).some((key) => key.toLowerCase() === "user-agent");
|
|
1310
|
+
return { ...init, headers: has ? record : { ...record, "User-Agent": userAgent } };
|
|
1311
|
+
}
|
|
1312
|
+
function fetchWithUserAgent(userAgent) {
|
|
1313
|
+
return ((input, init) => globalThis.fetch(input, withUserAgent(init, userAgent)));
|
|
1371
1314
|
}
|
|
1372
1315
|
|
|
1373
|
-
// src/
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1316
|
+
// src/client-info.ts
|
|
1317
|
+
var CLI_PACKAGE = { name: "groundmail-cli", version };
|
|
1318
|
+
|
|
1319
|
+
// src/session/http.ts
|
|
1320
|
+
var CliRequestError = class extends Error {
|
|
1321
|
+
};
|
|
1322
|
+
async function apiRequest(baseUrl, path, options = {}) {
|
|
1323
|
+
const doFetch = fetchWithUserAgent(buildUserAgent(CLI_PACKAGE));
|
|
1324
|
+
const url = `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
1325
|
+
let response;
|
|
1384
1326
|
try {
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1327
|
+
response = await doFetch(url, {
|
|
1328
|
+
method: options.method ?? (options.body === void 0 ? "GET" : "POST"),
|
|
1329
|
+
headers: {
|
|
1330
|
+
Accept: "application/json",
|
|
1331
|
+
...options.body === void 0 ? {} : { "Content-Type": "application/json" },
|
|
1332
|
+
...options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {}
|
|
1333
|
+
},
|
|
1334
|
+
...options.body === void 0 ? {} : { body: JSON.stringify(options.body) }
|
|
1390
1335
|
});
|
|
1391
|
-
console.log(JSON.stringify(result));
|
|
1392
|
-
return { exitCode: 0 };
|
|
1393
1336
|
} catch (error) {
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// src/commands/suppressions/remove.ts
|
|
1399
|
-
async function runRemoveCommand(args, env = process.env) {
|
|
1400
|
-
const options = parseSuppressionsCommonArgs(args);
|
|
1401
|
-
const apiKey = resolveApiKey6(options, env);
|
|
1402
|
-
if (!apiKey) {
|
|
1403
|
-
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
1404
|
-
}
|
|
1405
|
-
const idOrEmail = readFlag18(args, "--id") || readFlag18(args, "--email") || args.find((a) => !a.startsWith("-") && a !== "remove" && a !== "delete");
|
|
1406
|
-
if (!idOrEmail) {
|
|
1407
|
-
return { exitCode: 1, error: "Missing id or email. Pass --id <uuid> or --email user@example.com" };
|
|
1337
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1338
|
+
throw new CliRequestError(`Could not reach ${baseUrl}: ${reason}`);
|
|
1408
1339
|
}
|
|
1340
|
+
const text = await response.text();
|
|
1341
|
+
let body = {};
|
|
1409
1342
|
try {
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
} catch (error) {
|
|
1415
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1343
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
1344
|
+
body = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { value: parsed };
|
|
1345
|
+
} catch {
|
|
1346
|
+
body = { error: text.slice(0, 300) || `HTTP ${response.status}` };
|
|
1416
1347
|
}
|
|
1348
|
+
return { ok: response.ok, status: response.status, body };
|
|
1417
1349
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
function readFlag19(args, flag) {
|
|
1421
|
-
const index = args.indexOf(flag);
|
|
1422
|
-
if (index === -1) return void 0;
|
|
1423
|
-
return args[index + 1];
|
|
1424
|
-
}
|
|
1425
|
-
function parseTemplatesCommonArgs(args) {
|
|
1426
|
-
return {
|
|
1427
|
-
apiKey: readFlag19(args, "--api-key"),
|
|
1428
|
-
baseUrl: readFlag19(args, "--base-url")
|
|
1429
|
-
};
|
|
1430
|
-
}
|
|
1431
|
-
function resolveApiKey7(options, env) {
|
|
1432
|
-
return options.apiKey || env.GROUNDMAIL_API_KEY || null;
|
|
1433
|
-
}
|
|
1434
|
-
async function createTemplatesClient(apiKey, baseUrl) {
|
|
1435
|
-
const { Groundmail } = await import("groundmail");
|
|
1436
|
-
return new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
// src/commands/templates/delete.ts
|
|
1440
|
-
var POSITIONAL_FLAGS7 = ["--api-key", "--base-url", "--id", "--alias"];
|
|
1441
|
-
async function runDeleteCommand4(args, env = process.env) {
|
|
1442
|
-
const options = parseTemplatesCommonArgs(args);
|
|
1443
|
-
const apiKey = resolveApiKey7(options, env);
|
|
1444
|
-
if (!apiKey) return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
1445
|
-
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional8(args);
|
|
1446
|
-
if (!idOrAlias) return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
|
|
1447
|
-
try {
|
|
1448
|
-
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
1449
|
-
console.log(JSON.stringify(await client.templates.remove(idOrAlias)));
|
|
1450
|
-
return { exitCode: 0 };
|
|
1451
|
-
} catch (error) {
|
|
1452
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1453
|
-
}
|
|
1350
|
+
function sentence(text) {
|
|
1351
|
+
return /[.!?…)]$/.test(text.trim()) ? text.trim() : `${text.trim()}.`;
|
|
1454
1352
|
}
|
|
1455
|
-
function
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
}
|
|
1462
|
-
return arg;
|
|
1353
|
+
function errorMessage(response, fallback) {
|
|
1354
|
+
const error = response.body.error;
|
|
1355
|
+
if (typeof error === "string" && error.trim()) return error.trim();
|
|
1356
|
+
if (error && typeof error === "object") {
|
|
1357
|
+
const message = error.message;
|
|
1358
|
+
if (typeof message === "string" && message.trim()) return message.trim();
|
|
1463
1359
|
}
|
|
1464
|
-
return
|
|
1360
|
+
return `${fallback} (HTTP ${response.status}).`;
|
|
1465
1361
|
}
|
|
1466
1362
|
|
|
1467
|
-
// src/commands/
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
if (POSITIONAL_FLAGS8.includes(arg)) i += 1;
|
|
1363
|
+
// src/commands/login.ts
|
|
1364
|
+
import { spawn } from "child_process";
|
|
1365
|
+
import { hostname } from "os";
|
|
1366
|
+
|
|
1367
|
+
// src/session/args.ts
|
|
1368
|
+
var COMMON_VALUE_FLAGS = ["--api-key", "--base-url", "--workspace"];
|
|
1369
|
+
var COMMON_BOOLEAN_FLAGS = ["--json"];
|
|
1370
|
+
function parseArgs(argv, spec) {
|
|
1371
|
+
const valueFlags = new Set(spec.values ?? []);
|
|
1372
|
+
const booleanFlags = new Set(spec.booleans ?? []);
|
|
1373
|
+
const values = {};
|
|
1374
|
+
const booleans = {};
|
|
1375
|
+
const positionals = [];
|
|
1376
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1377
|
+
const arg = argv[i];
|
|
1378
|
+
if (arg === "--") {
|
|
1379
|
+
positionals.push(...argv.slice(i + 1));
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
if (!arg.startsWith("--") || arg === "-") {
|
|
1383
|
+
positionals.push(arg);
|
|
1489
1384
|
continue;
|
|
1490
1385
|
}
|
|
1491
|
-
|
|
1386
|
+
const eq = arg.indexOf("=");
|
|
1387
|
+
const name = eq === -1 ? arg : arg.slice(0, eq);
|
|
1388
|
+
if (valueFlags.has(name)) {
|
|
1389
|
+
const value = eq === -1 ? argv[i + 1] : arg.slice(eq + 1);
|
|
1390
|
+
if (value === void 0 || eq === -1 && value.startsWith("--")) {
|
|
1391
|
+
return { ok: false, error: `${name} needs a value.` };
|
|
1392
|
+
}
|
|
1393
|
+
values[name] = value;
|
|
1394
|
+
if (eq === -1) i++;
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
if (booleanFlags.has(name) && eq === -1) {
|
|
1398
|
+
booleans[name] = true;
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const negated = name.startsWith("--no-") ? `--${name.slice("--no-".length)}` : null;
|
|
1402
|
+
if (negated && booleanFlags.has(negated) && eq === -1) {
|
|
1403
|
+
booleans[negated] = false;
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const known = [...valueFlags, ...booleanFlags].sort().join(", ");
|
|
1407
|
+
return { ok: false, error: `Unknown flag ${name}.${known ? ` This command takes: ${known}.` : ""}` };
|
|
1492
1408
|
}
|
|
1493
|
-
return
|
|
1409
|
+
return { ok: true, args: { values, booleans, positionals } };
|
|
1410
|
+
}
|
|
1411
|
+
function wantsJson(args, stdout = process.stdout) {
|
|
1412
|
+
if (args.booleans["--json"] !== void 0) return args.booleans["--json"];
|
|
1413
|
+
return !stdout.isTTY;
|
|
1494
1414
|
}
|
|
1415
|
+
var EXIT_OK = 0;
|
|
1416
|
+
var EXIT_FAILED = 1;
|
|
1417
|
+
var EXIT_USAGE = 2;
|
|
1495
1418
|
|
|
1496
|
-
// src/
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
1506
|
-
console.log(JSON.stringify(await client.templates.get(idOrAlias)));
|
|
1507
|
-
return { exitCode: 0 };
|
|
1508
|
-
} catch (error) {
|
|
1509
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1510
|
-
}
|
|
1419
|
+
// src/session/config.ts
|
|
1420
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
1421
|
+
import { homedir } from "os";
|
|
1422
|
+
import { dirname, join } from "path";
|
|
1423
|
+
function configDir(env = process.env) {
|
|
1424
|
+
const override2 = env.GROUNDMAIL_CONFIG_DIR?.trim();
|
|
1425
|
+
if (override2) return override2;
|
|
1426
|
+
const base = env.XDG_CONFIG_HOME?.trim() || (process.platform === "win32" ? env.APPDATA?.trim() || join(homedir(), "AppData", "Roaming") : "") || join(homedir(), ".config");
|
|
1427
|
+
return join(base, cliBrand(env).configDirName);
|
|
1511
1428
|
}
|
|
1512
|
-
function
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1429
|
+
function configPath(env = process.env) {
|
|
1430
|
+
return join(configDir(env), "config.json");
|
|
1431
|
+
}
|
|
1432
|
+
function loadConfig(env = process.env) {
|
|
1433
|
+
try {
|
|
1434
|
+
const parsed = JSON.parse(readFileSync(configPath(env), "utf8"));
|
|
1435
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
1436
|
+
const record = parsed;
|
|
1437
|
+
const workspaces = {};
|
|
1438
|
+
for (const [id, entry] of Object.entries(record.workspaces ?? {})) {
|
|
1439
|
+
const hasKey = typeof entry?.apiKey === "string" && entry.apiKey.length > 0;
|
|
1440
|
+
const hasOAuth = Boolean(entry?.oauth && typeof entry.oauth === "object" && typeof entry.oauth.clientId === "string");
|
|
1441
|
+
if (entry && typeof entry.id === "string" && (hasKey || hasOAuth)) {
|
|
1442
|
+
workspaces[id] = entry;
|
|
1443
|
+
}
|
|
1518
1444
|
}
|
|
1519
|
-
return
|
|
1445
|
+
return {
|
|
1446
|
+
...typeof record.current === "string" && workspaces[record.current] ? { current: record.current } : {},
|
|
1447
|
+
workspaces,
|
|
1448
|
+
...record.pendingLogin && typeof record.pendingLogin.deviceCode === "string" ? { pendingLogin: record.pendingLogin } : {}
|
|
1449
|
+
};
|
|
1450
|
+
} catch {
|
|
1451
|
+
return {};
|
|
1520
1452
|
}
|
|
1521
|
-
return void 0;
|
|
1522
1453
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
}
|
|
1454
|
+
function saveConfig(config, env = process.env) {
|
|
1455
|
+
const file = configPath(env);
|
|
1456
|
+
mkdirSync(dirname(file), { recursive: true, mode: 448 });
|
|
1457
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
1458
|
+
writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
1459
|
+
`, { mode: 384 });
|
|
1460
|
+
renameSync(tmp, file);
|
|
1531
1461
|
try {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
const cursor = readFlag19(args, "--cursor");
|
|
1535
|
-
const status = readFlag19(args, "--status");
|
|
1536
|
-
const result = await client.templates.list({
|
|
1537
|
-
...limitRaw ? { limit: Number(limitRaw) } : {},
|
|
1538
|
-
...cursor ? { cursor } : {},
|
|
1539
|
-
...status ? { status } : {}
|
|
1540
|
-
});
|
|
1541
|
-
console.log(JSON.stringify(result));
|
|
1542
|
-
return { exitCode: 0 };
|
|
1543
|
-
} catch (error) {
|
|
1544
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1462
|
+
chmodSync(file, 384);
|
|
1463
|
+
} catch {
|
|
1545
1464
|
}
|
|
1546
1465
|
}
|
|
1466
|
+
function updateConfig(change, env = process.env) {
|
|
1467
|
+
const next = change(loadConfig(env));
|
|
1468
|
+
saveConfig(next, env);
|
|
1469
|
+
return next;
|
|
1470
|
+
}
|
|
1471
|
+
function savedWorkspaces(config) {
|
|
1472
|
+
return Object.values(config.workspaces ?? {}).sort((a, b) => a.name.localeCompare(b.name));
|
|
1473
|
+
}
|
|
1474
|
+
function findSavedWorkspace(config, wanted) {
|
|
1475
|
+
const all = savedWorkspaces(config);
|
|
1476
|
+
const key = wanted.trim();
|
|
1477
|
+
const byId = all.find((workspace) => workspace.id === key);
|
|
1478
|
+
if (byId) return { ok: true, workspace: byId };
|
|
1479
|
+
const byName = all.filter((workspace) => workspace.name.trim().toLowerCase() === key.toLowerCase());
|
|
1480
|
+
if (byName.length === 1) return { ok: true, workspace: byName[0] };
|
|
1481
|
+
return { ok: false, reason: byName.length ? "ambiguous" : "none", matches: byName };
|
|
1482
|
+
}
|
|
1547
1483
|
|
|
1548
|
-
// src/
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
if (
|
|
1556
|
-
|
|
1557
|
-
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
1558
|
-
console.log(JSON.stringify(await client.templates.publish(idOrAlias)));
|
|
1559
|
-
return { exitCode: 0 };
|
|
1560
|
-
} catch (error) {
|
|
1561
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1562
|
-
}
|
|
1484
|
+
// src/session/keychain.ts
|
|
1485
|
+
import { createRequire } from "module";
|
|
1486
|
+
var SERVICE = "groundmail-cli";
|
|
1487
|
+
var override;
|
|
1488
|
+
var cached;
|
|
1489
|
+
function platformLabel() {
|
|
1490
|
+
if (process.platform === "darwin") return "the macOS Keychain";
|
|
1491
|
+
if (process.platform === "win32") return "Windows Credential Manager";
|
|
1492
|
+
return "the system keyring";
|
|
1563
1493
|
}
|
|
1564
|
-
function
|
|
1565
|
-
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1494
|
+
function loadKeyring() {
|
|
1495
|
+
try {
|
|
1496
|
+
const require2 = createRequire(import.meta.url);
|
|
1497
|
+
return require2("@napi-rs/keyring");
|
|
1498
|
+
} catch {
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
function secretStore(env = process.env) {
|
|
1503
|
+
if (override !== void 0) return override;
|
|
1504
|
+
if (/^(1|true|yes)$/i.test(env.GROUNDMAIL_NO_KEYCHAIN?.trim() ?? "")) return null;
|
|
1505
|
+
if (cached !== void 0) return cached;
|
|
1506
|
+
const keyring = loadKeyring();
|
|
1507
|
+
if (!keyring) {
|
|
1508
|
+
cached = null;
|
|
1509
|
+
return cached;
|
|
1510
|
+
}
|
|
1511
|
+
cached = {
|
|
1512
|
+
label: platformLabel(),
|
|
1513
|
+
get(account) {
|
|
1514
|
+
try {
|
|
1515
|
+
return new keyring.Entry(SERVICE, account).getPassword() ?? null;
|
|
1516
|
+
} catch {
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
set(account, secret) {
|
|
1521
|
+
try {
|
|
1522
|
+
const entry = new keyring.Entry(SERVICE, account);
|
|
1523
|
+
entry.setPassword(secret);
|
|
1524
|
+
return entry.getPassword() === secret;
|
|
1525
|
+
} catch {
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1528
|
+
},
|
|
1529
|
+
delete(account) {
|
|
1530
|
+
try {
|
|
1531
|
+
new keyring.Entry(SERVICE, account).deletePassword();
|
|
1532
|
+
} catch {
|
|
1533
|
+
}
|
|
1570
1534
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
return void 0;
|
|
1535
|
+
};
|
|
1536
|
+
return cached;
|
|
1574
1537
|
}
|
|
1575
1538
|
|
|
1576
|
-
// src/
|
|
1577
|
-
var
|
|
1578
|
-
var
|
|
1579
|
-
var
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
return match ? match[0].toLowerCase() : "";
|
|
1539
|
+
// src/session/oauth.ts
|
|
1540
|
+
var CLI_OAUTH_CLIENT_ID = "groundmail-cli";
|
|
1541
|
+
var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1542
|
+
var EXPIRY_SKEW_MS = 60 * 1e3;
|
|
1543
|
+
function oauthErrorCode(response) {
|
|
1544
|
+
const error = response.body.error;
|
|
1545
|
+
return typeof error === "string" ? error : null;
|
|
1584
1546
|
}
|
|
1585
|
-
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1547
|
+
function oauthErrorMessage(response, fallback) {
|
|
1548
|
+
const description = response.body.error_description;
|
|
1549
|
+
if (typeof description === "string" && description.trim()) return description.trim();
|
|
1550
|
+
return errorMessage(response, fallback);
|
|
1588
1551
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
const { resolve } = await import("path");
|
|
1592
|
-
const url = pathToFileURL(resolve(filePath)).href;
|
|
1593
|
-
try {
|
|
1594
|
-
return await import(url);
|
|
1595
|
-
} catch (error) {
|
|
1596
|
-
if (!TYPESCRIPT_EXTENSIONS.includes(extensionOf(filePath))) throw error;
|
|
1597
|
-
try {
|
|
1598
|
-
const specifier = "tsx/esm/api";
|
|
1599
|
-
const tsx = await import(specifier);
|
|
1600
|
-
tsx.register();
|
|
1601
|
-
} catch {
|
|
1602
|
-
throw new Error(
|
|
1603
|
-
`Cannot import ${filePath} directly. Install tsx to push TypeScript or JSX templates (npm install -D tsx), or pre-render the file to .html.`
|
|
1604
|
-
);
|
|
1605
|
-
}
|
|
1606
|
-
return import(url);
|
|
1607
|
-
}
|
|
1552
|
+
function isTokenSet(body) {
|
|
1553
|
+
return typeof body.access_token === "string" && typeof body.refresh_token === "string";
|
|
1608
1554
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
const mod = await import("@react-email/render");
|
|
1612
|
-
const render = mod.render ?? mod.default?.render;
|
|
1613
|
-
if (typeof render !== "function") throw new Error("missing render export");
|
|
1614
|
-
return render;
|
|
1615
|
-
} catch {
|
|
1616
|
-
throw new Error(
|
|
1617
|
-
"Rendering a React email requires @react-email/render. Install it with: npm install @react-email/render react"
|
|
1618
|
-
);
|
|
1619
|
-
}
|
|
1555
|
+
function keychainAccount(apiUrl, workspaceId) {
|
|
1556
|
+
return `${apiUrl.replace(/\/+$/, "")}#${workspaceId}`;
|
|
1620
1557
|
}
|
|
1621
|
-
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
const react = await import("./react-IXBRBSFN.js");
|
|
1626
|
-
const createElement = react.createElement ?? react.default?.createElement;
|
|
1627
|
-
if (typeof createElement !== "function") {
|
|
1628
|
-
throw new Error("Rendering a React email requires react. Install it with: npm install react");
|
|
1629
|
-
}
|
|
1630
|
-
return createElement(exported, props);
|
|
1631
|
-
}
|
|
1632
|
-
if (!exported) {
|
|
1633
|
-
throw new Error("The template module must export a React component as its default export.");
|
|
1558
|
+
function storeRefreshToken(input, env = process.env) {
|
|
1559
|
+
const keychain = secretStore(env);
|
|
1560
|
+
if (keychain?.set(keychainAccount(input.apiUrl, input.workspaceId), input.refreshToken)) {
|
|
1561
|
+
return { clientId: CLI_OAUTH_CLIENT_ID, scope: input.scope, store: "keychain" };
|
|
1634
1562
|
}
|
|
1635
|
-
return
|
|
1563
|
+
return { clientId: CLI_OAUTH_CLIENT_ID, scope: input.scope, store: "file", refreshToken: input.refreshToken };
|
|
1636
1564
|
}
|
|
1637
|
-
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
return { html: await readFile3(filePath) };
|
|
1643
|
-
}
|
|
1644
|
-
if (TEXT_EXTENSIONS.includes(extension)) {
|
|
1645
|
-
return { text: await readFile3(filePath) };
|
|
1646
|
-
}
|
|
1647
|
-
if (!MODULE_EXTENSIONS.includes(extension)) {
|
|
1648
|
-
throw new Error(
|
|
1649
|
-
`Unsupported template file "${filePath}". Use .html, .txt, or a module exporting a React component (${MODULE_EXTENSIONS.join(", ")}).`
|
|
1650
|
-
);
|
|
1651
|
-
}
|
|
1652
|
-
const mod = await importModule(filePath);
|
|
1653
|
-
const element = await toElement(mod, options.props ?? {});
|
|
1654
|
-
const render = await loadReactEmailRender();
|
|
1655
|
-
const html = await render(element);
|
|
1656
|
-
let text;
|
|
1657
|
-
try {
|
|
1658
|
-
text = await render(element, { plainText: true });
|
|
1659
|
-
} catch {
|
|
1660
|
-
text = void 0;
|
|
1661
|
-
}
|
|
1662
|
-
return { html, ...text ? { text } : {} };
|
|
1565
|
+
function readRefreshToken(workspace, apiUrl, env = process.env) {
|
|
1566
|
+
const oauth = workspace.oauth;
|
|
1567
|
+
if (!oauth) return null;
|
|
1568
|
+
if (oauth.store === "file") return oauth.refreshToken ?? null;
|
|
1569
|
+
return secretStore(env)?.get(keychainAccount(apiUrl, workspace.id)) ?? null;
|
|
1663
1570
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
"
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
"
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1571
|
+
function forgetRefreshToken(workspace, apiUrl, env = process.env) {
|
|
1572
|
+
if (workspace.oauth?.store === "keychain") secretStore(env)?.delete(keychainAccount(apiUrl, workspace.id));
|
|
1573
|
+
}
|
|
1574
|
+
function storeLabel(oauth, configFile, env = process.env) {
|
|
1575
|
+
return oauth.store === "keychain" ? secretStore(env)?.label ?? "the system keychain" : `${configFile} (readable by you only)`;
|
|
1576
|
+
}
|
|
1577
|
+
async function refresh(apiUrl, refreshToken, scope, env) {
|
|
1578
|
+
return apiRequest(apiUrl, "/oauth/token", {
|
|
1579
|
+
body: {
|
|
1580
|
+
grant_type: "refresh_token",
|
|
1581
|
+
refresh_token: refreshToken,
|
|
1582
|
+
client_id: CLI_OAUTH_CLIENT_ID,
|
|
1583
|
+
...scope ? { scope } : {}
|
|
1584
|
+
},
|
|
1585
|
+
env
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
async function accessTokenFor(workspace, apiUrl, options = {}) {
|
|
1589
|
+
const env = options.env ?? process.env;
|
|
1590
|
+
const now = options.now ?? Date.now;
|
|
1591
|
+
const brand = cliBrand(env);
|
|
1592
|
+
const oauth = workspace.oauth;
|
|
1593
|
+
if (!oauth) return { ok: false, error: "Not an OAuth sign-in." };
|
|
1594
|
+
const narrowed = Boolean(options.scope && options.scope.trim() !== oauth.scope);
|
|
1595
|
+
if (!narrowed && oauth.accessToken && (oauth.accessTokenExpiresAt ?? 0) - EXPIRY_SKEW_MS > now()) {
|
|
1596
|
+
return { ok: true, token: oauth.accessToken, scope: oauth.scope };
|
|
1597
|
+
}
|
|
1598
|
+
const signInAgain = `Run ${brand.bin} login${workspace.name ? ` --workspace "${workspace.name}"` : ""} to sign in again.`;
|
|
1599
|
+
let refreshToken = readRefreshToken(workspace, apiUrl, env);
|
|
1600
|
+
if (!refreshToken) return { ok: false, error: `The saved sign-in for ${workspace.name} has no refresh token. ${signInAgain}` };
|
|
1601
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1602
|
+
const response = await refresh(apiUrl, refreshToken, narrowed ? options.scope.trim() : null, env);
|
|
1603
|
+
if (response.ok && isTokenSet(response.body)) {
|
|
1604
|
+
const tokens = response.body;
|
|
1605
|
+
const stored = storeRefreshToken({ apiUrl, workspaceId: workspace.id, refreshToken: tokens.refresh_token, scope: oauth.scope }, env);
|
|
1606
|
+
updateConfig((config) => {
|
|
1607
|
+
const current = config.workspaces?.[workspace.id];
|
|
1608
|
+
if (!current) return config;
|
|
1609
|
+
const next = {
|
|
1610
|
+
...stored,
|
|
1611
|
+
scope: oauth.scope,
|
|
1612
|
+
...narrowed ? { accessToken: current.oauth?.accessToken, accessTokenExpiresAt: current.oauth?.accessTokenExpiresAt } : { accessToken: tokens.access_token, accessTokenExpiresAt: now() + tokens.expires_in * 1e3 }
|
|
1613
|
+
};
|
|
1614
|
+
return { ...config, workspaces: { ...config.workspaces, [workspace.id]: { ...current, oauth: next } } };
|
|
1615
|
+
}, env);
|
|
1616
|
+
return { ok: true, token: tokens.access_token, scope: tokens.scope };
|
|
1617
|
+
}
|
|
1618
|
+
const code = oauthErrorCode(response);
|
|
1619
|
+
if (code === "invalid_grant" && attempt === 0) {
|
|
1620
|
+
const latest = loadConfig(env).workspaces?.[workspace.id];
|
|
1621
|
+
const latestToken = latest ? readRefreshToken(latest, apiUrl, env) : null;
|
|
1622
|
+
if (latest?.oauth && latestToken && latestToken !== refreshToken) {
|
|
1623
|
+
if (!narrowed && latest.oauth.accessToken && (latest.oauth.accessTokenExpiresAt ?? 0) - EXPIRY_SKEW_MS > now()) {
|
|
1624
|
+
return { ok: true, token: latest.oauth.accessToken, scope: latest.oauth.scope };
|
|
1625
|
+
}
|
|
1626
|
+
refreshToken = latestToken;
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1682
1629
|
}
|
|
1683
|
-
|
|
1630
|
+
if (code === "invalid_grant") {
|
|
1631
|
+
return { ok: false, error: `${oauthErrorMessage(response, "The sign-in is no longer valid")} ${signInAgain}`.trim() };
|
|
1632
|
+
}
|
|
1633
|
+
if (code === "invalid_scope") {
|
|
1634
|
+
return { ok: false, error: `${oauthErrorMessage(response, "That scope was not approved for this sign-in")} To get it: ${brand.bin} login --scope "${options.scope ?? ""}".` };
|
|
1635
|
+
}
|
|
1636
|
+
return { ok: false, error: oauthErrorMessage(response, "Could not refresh the access token") };
|
|
1684
1637
|
}
|
|
1685
|
-
return
|
|
1638
|
+
return { ok: false, error: `The sign-in is no longer valid. ${signInAgain}` };
|
|
1686
1639
|
}
|
|
1687
|
-
function
|
|
1688
|
-
|
|
1689
|
-
|
|
1640
|
+
async function revokeRefreshToken(apiUrl, refreshToken, env = process.env) {
|
|
1641
|
+
return apiRequest(apiUrl, "/oauth/revoke", {
|
|
1642
|
+
body: { token: refreshToken, token_type_hint: "refresh_token", client_id: CLI_OAUTH_CLIENT_ID },
|
|
1643
|
+
env
|
|
1644
|
+
});
|
|
1690
1645
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1646
|
+
|
|
1647
|
+
// src/session/credentials.ts
|
|
1648
|
+
function envApiKey(env = process.env) {
|
|
1649
|
+
return env.GROUNDMAIL_API_KEY?.trim() || null;
|
|
1650
|
+
}
|
|
1651
|
+
function currentSavedWorkspace(config, env = process.env) {
|
|
1652
|
+
const brand = cliBrand(env);
|
|
1653
|
+
const all = savedWorkspaces(config);
|
|
1654
|
+
if (config.current && config.workspaces?.[config.current]) {
|
|
1655
|
+
return { ok: true, workspace: config.workspaces[config.current] };
|
|
1696
1656
|
}
|
|
1697
|
-
|
|
1698
|
-
if (
|
|
1699
|
-
return {
|
|
1657
|
+
if (all.length === 1) return { ok: true, workspace: all[0] };
|
|
1658
|
+
if (all.length === 0) {
|
|
1659
|
+
return {
|
|
1660
|
+
ok: false,
|
|
1661
|
+
reason: "none",
|
|
1662
|
+
error: `Not signed in. Run: ${brand.bin} login (it prints a link to approve in the browser). Or set ${brand.keyEnvVar}.`
|
|
1663
|
+
};
|
|
1700
1664
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1665
|
+
return {
|
|
1666
|
+
ok: false,
|
|
1667
|
+
reason: "ambiguous",
|
|
1668
|
+
error: `Signed in to ${all.length} workspaces on this computer and none is picked: ${all.map((workspace) => `"${workspace.name}"`).join(", ")}. Pick one: ${brand.bin} workspaces use <name>, or pass --workspace <name>.`
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
function savedApiUrl(workspace, env = process.env, flagUrl) {
|
|
1672
|
+
return trimUrl(flagUrl) || explicitApiUrl(env) || trimUrl(workspace?.apiUrl) || defaultApiUrl(env);
|
|
1673
|
+
}
|
|
1674
|
+
async function savedCredential(workspace, env = process.env) {
|
|
1675
|
+
if (workspace.oauth) {
|
|
1676
|
+
const issuer = trimUrl(workspace.apiUrl) || defaultApiUrl(env);
|
|
1677
|
+
const token = await accessTokenFor(workspace, issuer, { env });
|
|
1678
|
+
return token.ok ? { ok: true, token: token.token, kind: "oauth" } : token;
|
|
1704
1679
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
const
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1680
|
+
if (workspace.apiKey) return { ok: true, token: workspace.apiKey, kind: "api_key" };
|
|
1681
|
+
return { ok: false, error: `The saved sign-in for ${workspace.name} has no credential. Run ${cliBrand(env).bin} login.` };
|
|
1682
|
+
}
|
|
1683
|
+
async function resolveAuth(flags, env = process.env, config = loadConfig(env)) {
|
|
1684
|
+
const brand = cliBrand(env);
|
|
1685
|
+
const flagUrl = trimUrl(flags.baseUrl) || null;
|
|
1686
|
+
const urlFor = (workspace) => savedApiUrl(workspace, env, flagUrl);
|
|
1687
|
+
const flagKey = flags.apiKey?.trim();
|
|
1688
|
+
if (flagKey) {
|
|
1689
|
+
return { ok: true, auth: { apiKey: flagKey, baseUrl: urlFor(null), source: "flag", kind: "api_key", workspace: null } };
|
|
1690
|
+
}
|
|
1691
|
+
const fromSaved = async (workspace) => {
|
|
1692
|
+
const baseUrl = urlFor(workspace);
|
|
1693
|
+
const credential = await savedCredential(workspace, env);
|
|
1694
|
+
if (!credential.ok) return credential;
|
|
1695
|
+
return { ok: true, auth: { apiKey: credential.token, baseUrl, source: "saved", kind: credential.kind, workspace } };
|
|
1696
|
+
};
|
|
1697
|
+
if (flags.workspace?.trim()) {
|
|
1698
|
+
const match = findSavedWorkspace(config, flags.workspace);
|
|
1699
|
+
if (!match.ok) {
|
|
1700
|
+
const saved = savedWorkspaces(config);
|
|
1701
|
+
return {
|
|
1702
|
+
ok: false,
|
|
1703
|
+
error: match.reason === "ambiguous" ? `More than one saved workspace is called "${flags.workspace}". Use its id: ${match.matches.map((w) => w.id).join(", ")}.` : `Not signed in to "${flags.workspace}" on this computer.${saved.length ? ` Signed in: ${saved.map((w) => `"${w.name}"`).join(", ")}.` : ""} To add it: ${brand.bin} login --workspace "${flags.workspace}".`
|
|
1704
|
+
};
|
|
1721
1705
|
}
|
|
1706
|
+
return fromSaved(match.workspace);
|
|
1722
1707
|
}
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
...source.text ? { text: source.text } : {},
|
|
1745
|
-
...subject ? { subject } : {},
|
|
1746
|
-
...preheader ? { preheader } : {},
|
|
1747
|
-
...name ? { name } : {}
|
|
1748
|
-
};
|
|
1749
|
-
let result;
|
|
1750
|
-
if (existed) {
|
|
1751
|
-
result = await client.templates.update(alias, body);
|
|
1752
|
-
} else {
|
|
1753
|
-
if (!subject) {
|
|
1754
|
-
return {
|
|
1755
|
-
exitCode: 1,
|
|
1756
|
-
error: `Template "${alias}" does not exist yet. Pass --subject to create it.`
|
|
1757
|
-
};
|
|
1758
|
-
}
|
|
1759
|
-
result = await client.templates.create({
|
|
1760
|
-
name: name || alias,
|
|
1761
|
-
subject,
|
|
1762
|
-
alias,
|
|
1763
|
-
...body
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
if (shouldPublish) {
|
|
1767
|
-
result = await client.templates.publish(alias);
|
|
1708
|
+
const fromEnv = envApiKey(env);
|
|
1709
|
+
if (fromEnv) {
|
|
1710
|
+
return { ok: true, auth: { apiKey: fromEnv, baseUrl: urlFor(null), source: "env", kind: "api_key", workspace: null } };
|
|
1711
|
+
}
|
|
1712
|
+
const current = currentSavedWorkspace(config, env);
|
|
1713
|
+
if (!current.ok) return { ok: false, error: current.error };
|
|
1714
|
+
return fromSaved(current.workspace);
|
|
1715
|
+
}
|
|
1716
|
+
async function applySavedSession(argv, env = process.env, config) {
|
|
1717
|
+
const hasFlag = (flag) => argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
1718
|
+
let workspace = null;
|
|
1719
|
+
let notice;
|
|
1720
|
+
if (!hasFlag("--api-key") && !envApiKey(env)) {
|
|
1721
|
+
const current = currentSavedWorkspace(config ?? loadConfig(env), env);
|
|
1722
|
+
if (current.ok) {
|
|
1723
|
+
workspace = current.workspace;
|
|
1724
|
+
const credential = await savedCredential(current.workspace, env);
|
|
1725
|
+
if (credential.ok) env.GROUNDMAIL_API_KEY = credential.token;
|
|
1726
|
+
else notice = credential.error;
|
|
1727
|
+
} else if (current.reason === "ambiguous") {
|
|
1728
|
+
notice = current.error;
|
|
1768
1729
|
}
|
|
1769
|
-
console.log(JSON.stringify({ ...result, pushed: { file, alias, created: !existed } }));
|
|
1770
|
-
return { exitCode: 0 };
|
|
1771
|
-
} catch (error) {
|
|
1772
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1773
1730
|
}
|
|
1731
|
+
if (hasFlag("--base-url")) return { argv, notice };
|
|
1732
|
+
const url = explicitApiUrl(env) || trimUrl(workspace?.apiUrl) || trimUrl(env.GROUNDMAIL_DEFAULT_API_URL);
|
|
1733
|
+
return { argv: url ? [...argv, "--base-url", url] : argv, notice };
|
|
1774
1734
|
}
|
|
1775
1735
|
|
|
1776
|
-
// src/
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
const subject = readFlag19(args, "--subject");
|
|
1785
|
-
const html = readFlag19(args, "--html");
|
|
1786
|
-
const text = readFlag19(args, "--text");
|
|
1787
|
-
const alias = readFlag19(args, "--alias");
|
|
1788
|
-
const category = readFlag19(args, "--category");
|
|
1789
|
-
const unsubscribeEnabled = args.includes("--unsubscribe-enabled");
|
|
1790
|
-
if (!name) {
|
|
1791
|
-
return { exitCode: 1, error: 'Missing name. Pass --name "Welcome"' };
|
|
1792
|
-
}
|
|
1793
|
-
if (!subject) {
|
|
1794
|
-
return { exitCode: 1, error: 'Missing subject. Pass --subject "Hello {{name}}"' };
|
|
1736
|
+
// src/session/io.ts
|
|
1737
|
+
var processIo = {
|
|
1738
|
+
out: (text) => process.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
1739
|
+
`),
|
|
1740
|
+
err: (text) => process.stderr.write(text.endsWith("\n") ? text : `${text}
|
|
1741
|
+
`),
|
|
1742
|
+
get isTTY() {
|
|
1743
|
+
return Boolean(process.stdout.isTTY);
|
|
1795
1744
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1745
|
+
};
|
|
1746
|
+
function printJson(io, value) {
|
|
1747
|
+
io.out(JSON.stringify(value, null, 2));
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/commands/login.ts
|
|
1751
|
+
var realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1752
|
+
var CLI_SCOPES = [
|
|
1753
|
+
"full",
|
|
1754
|
+
"emails:send",
|
|
1755
|
+
"emails:read",
|
|
1756
|
+
"domains:read",
|
|
1757
|
+
"domains:write",
|
|
1758
|
+
"webhooks:write",
|
|
1759
|
+
"templates:write",
|
|
1760
|
+
"suppressions:write",
|
|
1761
|
+
"account:read"
|
|
1762
|
+
];
|
|
1763
|
+
function parseScopeFlag(value) {
|
|
1764
|
+
const tokens = String(value ?? "").split(/[\s,]+/).map((token) => token.trim()).filter(Boolean);
|
|
1765
|
+
if (!tokens.length) return { ok: true, scope: "full" };
|
|
1766
|
+
const unknown = tokens.filter((token) => !CLI_SCOPES.includes(token));
|
|
1767
|
+
if (unknown.length) {
|
|
1768
|
+
return { ok: false, error: `Unknown scope: ${unknown.join(", ")}. Scopes: ${CLI_SCOPES.join(", ")}.` };
|
|
1798
1769
|
}
|
|
1770
|
+
if (tokens.includes("full")) return { ok: true, scope: "full" };
|
|
1771
|
+
return { ok: true, scope: CLI_SCOPES.filter((scope) => tokens.includes(scope)).join(" ") };
|
|
1772
|
+
}
|
|
1773
|
+
function openInBrowser(url) {
|
|
1774
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
1799
1775
|
try {
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1802
|
-
name,
|
|
1803
|
-
subject,
|
|
1804
|
-
...html ? { html } : {},
|
|
1805
|
-
...text ? { text } : {},
|
|
1806
|
-
...alias ? { alias } : {},
|
|
1807
|
-
...category ? { category } : {},
|
|
1808
|
-
...unsubscribeEnabled ? { unsubscribe_enabled: true } : {}
|
|
1776
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
1777
|
+
child.on("error", () => {
|
|
1809
1778
|
});
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
} catch (error) {
|
|
1813
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1779
|
+
child.unref();
|
|
1780
|
+
} catch {
|
|
1814
1781
|
}
|
|
1815
1782
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
"
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
const
|
|
1832
|
-
const
|
|
1783
|
+
function sourceLabel(source, env) {
|
|
1784
|
+
if (source === "flag") return "--api-key";
|
|
1785
|
+
if (source === "env") return cliBrand(env).keyEnvVar;
|
|
1786
|
+
return "saved login";
|
|
1787
|
+
}
|
|
1788
|
+
function defaultDeviceName() {
|
|
1789
|
+
try {
|
|
1790
|
+
return hostname().slice(0, 80) || "this computer";
|
|
1791
|
+
} catch {
|
|
1792
|
+
return "this computer";
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
var dropPending = (env) => updateConfig(({ pendingLogin: _drop, ...rest }) => rest, env);
|
|
1796
|
+
async function runLoginCommand(argv, deps = {}) {
|
|
1797
|
+
const io = deps.io ?? processIo;
|
|
1798
|
+
const env = deps.env ?? process.env;
|
|
1799
|
+
const sleep = deps.sleep ?? realSleep;
|
|
1800
|
+
const now = deps.now ?? Date.now;
|
|
1801
|
+
const brand = cliBrand(env);
|
|
1802
|
+
const usage = `Usage: ${brand.bin} login [--scope <scopes>] [--workspace <name>] [--new] [--no-browser] [--wait|--no-wait] [--json]`;
|
|
1803
|
+
const parsed = parseArgs(argv, {
|
|
1804
|
+
values: ["--base-url", "--workspace", "--scope", "--device-name"],
|
|
1805
|
+
booleans: ["--json", "--browser", "--wait", "--new"]
|
|
1806
|
+
});
|
|
1807
|
+
if (!parsed.ok) return { exitCode: EXIT_USAGE, error: `${parsed.error}
|
|
1808
|
+
${usage}` };
|
|
1809
|
+
const { args } = parsed;
|
|
1810
|
+
if (args.positionals.length) {
|
|
1811
|
+
return { exitCode: EXIT_USAGE, error: `${brand.bin} login takes no arguments. To sign in to a workspace by name: ${brand.bin} login --workspace "<name>".` };
|
|
1812
|
+
}
|
|
1813
|
+
const scopeFlag = parseScopeFlag(args.values["--scope"]);
|
|
1814
|
+
if (!scopeFlag.ok) return { exitCode: EXIT_USAGE, error: `${scopeFlag.error}
|
|
1815
|
+
${usage}` };
|
|
1816
|
+
const scope = scopeFlag.scope;
|
|
1817
|
+
const json = wantsJson(args, io);
|
|
1818
|
+
const wait = args.booleans["--wait"] ?? io.isTTY;
|
|
1819
|
+
const browser = args.booleans["--browser"] ?? true;
|
|
1820
|
+
const apiUrl = trimUrl(args.values["--base-url"]) || explicitApiUrl(env) || defaultApiUrl(env);
|
|
1821
|
+
const workspaceHint = args.values["--workspace"]?.trim() || null;
|
|
1822
|
+
const deviceName = args.values["--device-name"]?.trim() || (deps.deviceName ?? defaultDeviceName)();
|
|
1823
|
+
try {
|
|
1824
|
+
const config = loadConfig(env);
|
|
1825
|
+
let login = config.pendingLogin;
|
|
1826
|
+
const resumed = Boolean(
|
|
1827
|
+
!args.booleans["--new"] && login && login.kind === "oauth" && login.apiUrl === apiUrl && login.expiresAt > now() + 5e3 && (login.workspace ?? null) === workspaceHint && (login.scope ?? "full") === scope
|
|
1828
|
+
);
|
|
1829
|
+
if (!resumed && !workspaceHint && !args.booleans["--new"] && !args.values["--scope"]) {
|
|
1830
|
+
const current = currentSavedWorkspace(config, env);
|
|
1831
|
+
if (current.ok && current.workspace.oauth && (trimUrl(current.workspace.apiUrl) || defaultApiUrl(env)) === apiUrl) {
|
|
1832
|
+
const next = `To sign in to another workspace: ${brand.bin} login --workspace "<name>". For other scopes: ${brand.bin} login --scope <scopes>. To sign in as someone else: ${brand.bin} logout, then ${brand.bin} login.`;
|
|
1833
|
+
if (json) {
|
|
1834
|
+
printJson(io, {
|
|
1835
|
+
status: "signed_in",
|
|
1836
|
+
already: true,
|
|
1837
|
+
workspace: { id: current.workspace.id, name: current.workspace.name },
|
|
1838
|
+
email: current.workspace.email ?? null,
|
|
1839
|
+
scope: current.workspace.oauth.scope,
|
|
1840
|
+
config_path: configPath(env),
|
|
1841
|
+
next
|
|
1842
|
+
});
|
|
1843
|
+
} else {
|
|
1844
|
+
io.out(`Already signed in${current.workspace.email ? ` as ${current.workspace.email}` : ""} to ${current.workspace.name}.
|
|
1845
|
+
${next}`);
|
|
1846
|
+
}
|
|
1847
|
+
return { exitCode: EXIT_OK };
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
if (!resumed) {
|
|
1851
|
+
const start = await apiRequest(apiUrl, "/oauth/device_authorization", {
|
|
1852
|
+
body: {
|
|
1853
|
+
client_id: CLI_OAUTH_CLIENT_ID,
|
|
1854
|
+
scope,
|
|
1855
|
+
device_name: deviceName,
|
|
1856
|
+
...workspaceHint ? { workspace: workspaceHint } : {}
|
|
1857
|
+
},
|
|
1858
|
+
env
|
|
1859
|
+
});
|
|
1860
|
+
if (!start.ok) return { exitCode: EXIT_FAILED, error: oauthErrorMessage(start, "Could not start the sign-in") };
|
|
1861
|
+
const body = start.body;
|
|
1862
|
+
const url = typeof body.verification_uri_complete === "string" ? body.verification_uri_complete : body.verification_uri;
|
|
1863
|
+
if (typeof body.device_code !== "string" || typeof body.user_code !== "string" || typeof url !== "string") {
|
|
1864
|
+
return { exitCode: EXIT_FAILED, error: `Unexpected answer from ${apiUrl}/oauth/device_authorization. Is --base-url right?` };
|
|
1865
|
+
}
|
|
1866
|
+
login = {
|
|
1867
|
+
kind: "oauth",
|
|
1868
|
+
scope,
|
|
1869
|
+
deviceCode: body.device_code,
|
|
1870
|
+
userCode: body.user_code,
|
|
1871
|
+
url,
|
|
1872
|
+
interval: Math.max(1, Number(body.interval) || 5),
|
|
1873
|
+
expiresAt: now() + (Number(body.expires_in) || 600) * 1e3,
|
|
1874
|
+
apiUrl,
|
|
1875
|
+
workspace: workspaceHint
|
|
1876
|
+
};
|
|
1877
|
+
const pending = login;
|
|
1878
|
+
updateConfig((current) => ({ ...current, pendingLogin: pending }), env);
|
|
1879
|
+
}
|
|
1880
|
+
const active = login;
|
|
1881
|
+
const link = `${resumed ? "Still waiting for approval" : `Sign in to ${brand.product}`}: open this link and approve code ${active.userCode}
|
|
1882
|
+
${active.url}`;
|
|
1883
|
+
if (!json) {
|
|
1884
|
+
io.out(`${link}${wait ? "\nWaiting\u2026 (Ctrl-C to stop; run the same command again to pick up where you left off)" : ""}`);
|
|
1885
|
+
} else if (wait) {
|
|
1886
|
+
io.err(link);
|
|
1887
|
+
}
|
|
1888
|
+
if (browser && !resumed && !env.CI) (deps.openBrowser ?? openInBrowser)(active.url);
|
|
1889
|
+
let interval = active.interval;
|
|
1890
|
+
for (; ; ) {
|
|
1891
|
+
const poll = await apiRequest(apiUrl, "/oauth/token", {
|
|
1892
|
+
body: { grant_type: DEVICE_CODE_GRANT_TYPE, device_code: active.deviceCode, client_id: CLI_OAUTH_CLIENT_ID },
|
|
1893
|
+
env
|
|
1894
|
+
});
|
|
1895
|
+
if (poll.ok && isTokenSet(poll.body)) {
|
|
1896
|
+
return finishLogin(poll.body, active, { io, env, json, now });
|
|
1897
|
+
}
|
|
1898
|
+
const code = oauthErrorCode(poll);
|
|
1899
|
+
if (code === "slow_down") interval += 5;
|
|
1900
|
+
else if (code === "access_denied") {
|
|
1901
|
+
dropPending(env);
|
|
1902
|
+
return {
|
|
1903
|
+
exitCode: EXIT_FAILED,
|
|
1904
|
+
error: `${sentence(oauthErrorMessage(poll, "The sign-in was canceled in the browser"))} Run ${brand.bin} login to start again.`
|
|
1905
|
+
};
|
|
1906
|
+
} else if (code !== "authorization_pending") {
|
|
1907
|
+
if (poll.status < 500 && poll.status !== 429) dropPending(env);
|
|
1908
|
+
if (code === "expired_token" || code === "invalid_grant") {
|
|
1909
|
+
return { exitCode: EXIT_FAILED, error: `The sign-in code expired or was already used. Run ${brand.bin} login again.` };
|
|
1910
|
+
}
|
|
1911
|
+
return { exitCode: EXIT_FAILED, error: oauthErrorMessage(poll, "The sign-in check failed") };
|
|
1912
|
+
}
|
|
1913
|
+
if (!wait || now() + interval * 1e3 > active.expiresAt) {
|
|
1914
|
+
const next = `Not signed in yet. Approve code ${active.userCode} in the browser, then run ${brand.bin} login again to finish.`;
|
|
1915
|
+
if (json) {
|
|
1916
|
+
printJson(io, {
|
|
1917
|
+
status: "pending",
|
|
1918
|
+
user_code: active.userCode,
|
|
1919
|
+
verification_url: active.url,
|
|
1920
|
+
expires_at: new Date(active.expiresAt).toISOString(),
|
|
1921
|
+
next
|
|
1922
|
+
});
|
|
1923
|
+
} else {
|
|
1924
|
+
io.out(next);
|
|
1925
|
+
}
|
|
1926
|
+
return { exitCode: EXIT_OK };
|
|
1927
|
+
}
|
|
1928
|
+
await sleep(interval * 1e3);
|
|
1929
|
+
}
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
if (error instanceof CliRequestError) return { exitCode: EXIT_FAILED, error: error.message };
|
|
1932
|
+
throw error;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
async function finishLogin(tokens, login, { io, env, json, now }) {
|
|
1936
|
+
const brand = cliBrand(env);
|
|
1937
|
+
const workspace = tokens.workspace ?? {};
|
|
1938
|
+
const user = tokens.user ?? {};
|
|
1939
|
+
const id = typeof workspace.id === "string" && workspace.id ? workspace.id : "default";
|
|
1940
|
+
const scope = typeof tokens.scope === "string" && tokens.scope ? tokens.scope : login.scope ?? "full";
|
|
1941
|
+
const previous = loadConfig(env).workspaces?.[id];
|
|
1942
|
+
let replacedKey = false;
|
|
1943
|
+
if (previous?.apiKey) {
|
|
1944
|
+
const response = await apiRequest(login.apiUrl, "/cli/session", { method: "DELETE", apiKey: previous.apiKey, env }).catch(() => null);
|
|
1945
|
+
replacedKey = Boolean(response?.ok && response.body.revoked === true);
|
|
1946
|
+
}
|
|
1947
|
+
if (previous?.oauth) {
|
|
1948
|
+
const oldRefresh = readRefreshToken(previous, login.apiUrl, env);
|
|
1949
|
+
if (oldRefresh) await revokeRefreshToken(login.apiUrl, oldRefresh, env).catch(() => null);
|
|
1950
|
+
}
|
|
1951
|
+
const stored = storeRefreshToken({ apiUrl: login.apiUrl, workspaceId: id, refreshToken: tokens.refresh_token, scope }, env);
|
|
1952
|
+
const saved = {
|
|
1953
|
+
id,
|
|
1954
|
+
name: typeof workspace.name === "string" && workspace.name ? workspace.name : "Workspace",
|
|
1955
|
+
oauth: { ...stored, accessToken: tokens.access_token, accessTokenExpiresAt: now() + tokens.expires_in * 1e3 },
|
|
1956
|
+
...login.apiUrl !== defaultApiUrl(env) ? { apiUrl: login.apiUrl } : {},
|
|
1957
|
+
email: typeof user.email === "string" ? user.email : null,
|
|
1958
|
+
savedAt: new Date(now()).toISOString()
|
|
1959
|
+
};
|
|
1960
|
+
const config = updateConfig(({ pendingLogin: _drop, ...rest }) => ({
|
|
1961
|
+
...rest,
|
|
1962
|
+
workspaces: { ...rest.workspaces ?? {}, [id]: saved },
|
|
1963
|
+
current: id
|
|
1964
|
+
}), env);
|
|
1965
|
+
const others = savedWorkspaces(config).filter((entry) => entry.id !== id);
|
|
1966
|
+
const envKey = envApiKey(env);
|
|
1967
|
+
const envNote = envKey ? `${sourceLabel("env", env)} is set in this environment and is used instead of this sign-in until you unset it.` : null;
|
|
1968
|
+
const where = storeLabel(saved.oauth, configPath(env), env);
|
|
1969
|
+
if (json) {
|
|
1970
|
+
printJson(io, {
|
|
1971
|
+
status: "signed_in",
|
|
1972
|
+
user: tokens.user ?? null,
|
|
1973
|
+
workspace: { id: saved.id, name: saved.name },
|
|
1974
|
+
scope,
|
|
1975
|
+
refresh_token_store: stored.store,
|
|
1976
|
+
config_path: configPath(env),
|
|
1977
|
+
other_workspaces: others.map((entry) => ({ id: entry.id, name: entry.name })),
|
|
1978
|
+
...replacedKey ? { replaced_api_key: true } : {},
|
|
1979
|
+
...envNote ? { note: envNote } : {}
|
|
1980
|
+
});
|
|
1981
|
+
return { exitCode: EXIT_OK };
|
|
1982
|
+
}
|
|
1983
|
+
const lines = [
|
|
1984
|
+
`Signed in${saved.email ? ` as ${saved.email}` : ""} to ${saved.name}.`,
|
|
1985
|
+
`Access: ${scope === "full" ? "full" : scope.split(" ").join(", ")}. Tokens refresh on their own; the refresh token is in ${where}.`
|
|
1986
|
+
];
|
|
1987
|
+
if (replacedKey) lines.push("The API key an earlier login saved was revoked; this sign-in replaces it.");
|
|
1988
|
+
if (others.length) {
|
|
1989
|
+
lines.push(
|
|
1990
|
+
`Also signed in here: ${others.map((entry) => entry.name).join(", ")}. Switch with ${brand.bin} workspaces use <name>.`
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
if (envNote) lines.push(envNote);
|
|
1994
|
+
lines.push(`Next: ${brand.bin} domains list`);
|
|
1995
|
+
io.out(lines.join("\n"));
|
|
1996
|
+
return { exitCode: EXIT_OK };
|
|
1997
|
+
}
|
|
1998
|
+
async function runLogoutCommand(argv, deps = {}) {
|
|
1999
|
+
const io = deps.io ?? processIo;
|
|
2000
|
+
const env = deps.env ?? process.env;
|
|
2001
|
+
const brand = cliBrand(env);
|
|
2002
|
+
const parsed = parseArgs(argv, { values: ["--workspace", "--base-url"], booleans: ["--json", "--all"] });
|
|
2003
|
+
if (!parsed.ok) return { exitCode: EXIT_USAGE, error: `${parsed.error}
|
|
2004
|
+
Usage: ${brand.bin} logout [--workspace <name>] [--all]` };
|
|
2005
|
+
const { args } = parsed;
|
|
2006
|
+
const json = wantsJson(args, io);
|
|
2007
|
+
const config = loadConfig(env);
|
|
2008
|
+
const all = savedWorkspaces(config);
|
|
2009
|
+
let targets;
|
|
2010
|
+
if (args.booleans["--all"]) {
|
|
2011
|
+
targets = all;
|
|
2012
|
+
} else if (args.values["--workspace"]) {
|
|
2013
|
+
const match = findSavedWorkspace(config, args.values["--workspace"]);
|
|
2014
|
+
if (!match.ok) {
|
|
2015
|
+
return { exitCode: EXIT_FAILED, error: `Not signed in to "${args.values["--workspace"]}" on this computer.` };
|
|
2016
|
+
}
|
|
2017
|
+
targets = [match.workspace];
|
|
2018
|
+
} else {
|
|
2019
|
+
const current = config.current ? config.workspaces?.[config.current] : all.length === 1 ? all[0] : void 0;
|
|
2020
|
+
if (!current && all.length > 1) {
|
|
2021
|
+
return {
|
|
2022
|
+
exitCode: EXIT_USAGE,
|
|
2023
|
+
error: `Signed in to ${all.length} workspaces and none is picked. Say which: ${brand.bin} logout --workspace <name>, or ${brand.bin} logout --all.`
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
targets = current ? [current] : [];
|
|
2027
|
+
}
|
|
2028
|
+
const results = [];
|
|
2029
|
+
for (const workspace of targets) {
|
|
2030
|
+
const apiUrl = trimUrl(args.values["--base-url"]) || explicitApiUrl(env) || trimUrl(workspace.apiUrl) || defaultApiUrl(env);
|
|
2031
|
+
let revoked = false;
|
|
2032
|
+
let warning;
|
|
2033
|
+
try {
|
|
2034
|
+
if (workspace.oauth) {
|
|
2035
|
+
const refreshToken = readRefreshToken(workspace, apiUrl, env);
|
|
2036
|
+
if (refreshToken) {
|
|
2037
|
+
const response = await revokeRefreshToken(apiUrl, refreshToken, env);
|
|
2038
|
+
revoked = response.ok;
|
|
2039
|
+
if (!response.ok) warning = oauthErrorMessage(response, "Could not revoke the sign-in");
|
|
2040
|
+
}
|
|
2041
|
+
forgetRefreshToken(workspace, apiUrl, env);
|
|
2042
|
+
} else if (workspace.apiKey) {
|
|
2043
|
+
const response = await apiRequest(apiUrl, "/cli/session", { method: "DELETE", apiKey: workspace.apiKey, env });
|
|
2044
|
+
revoked = response.ok && response.body.revoked === true;
|
|
2045
|
+
if (!response.ok && response.status !== 401) warning = errorMessage(response, "Could not revoke the key");
|
|
2046
|
+
}
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
warning = error instanceof Error ? error.message : String(error);
|
|
2049
|
+
}
|
|
2050
|
+
results.push({ id: workspace.id, name: workspace.name, revoked, ...warning ? { warning } : {} });
|
|
2051
|
+
}
|
|
2052
|
+
const removing = new Set(targets.map((workspace) => workspace.id));
|
|
2053
|
+
const hadPending = Boolean(config.pendingLogin);
|
|
2054
|
+
if (removing.size || hadPending) {
|
|
2055
|
+
updateConfig((current) => {
|
|
2056
|
+
const workspaces = Object.fromEntries(
|
|
2057
|
+
Object.entries(current.workspaces ?? {}).filter(([id]) => !removing.has(id))
|
|
2058
|
+
);
|
|
2059
|
+
const { pendingLogin: _drop, current: picked, ...rest } = current;
|
|
2060
|
+
return { ...rest, workspaces, ...picked && !removing.has(picked) ? { current: picked } : {} };
|
|
2061
|
+
}, env);
|
|
2062
|
+
}
|
|
2063
|
+
const envNote = envApiKey(env) ? `${sourceLabel("env", env)} is still set in this environment, so commands keep using it.` : null;
|
|
2064
|
+
if (json) {
|
|
2065
|
+
printJson(io, { signed_out: results, config_path: configPath(env), ...envNote ? { note: envNote } : {} });
|
|
2066
|
+
return { exitCode: EXIT_OK };
|
|
2067
|
+
}
|
|
2068
|
+
if (!results.length) {
|
|
2069
|
+
io.out(`Not signed in on this computer.${envNote ? ` ${envNote}` : ""}`);
|
|
2070
|
+
return { exitCode: EXIT_OK };
|
|
2071
|
+
}
|
|
2072
|
+
const lines = results.map((result) => {
|
|
2073
|
+
const target = targets.find((workspace) => workspace.id === result.id);
|
|
2074
|
+
const what = target?.oauth ? "the sign-in" : "the key";
|
|
2075
|
+
const revokeHint = target?.oauth ? "Revoke it in Settings \u2192 CLIs & agents." : "Revoke it on the API keys page.";
|
|
2076
|
+
return `Signed out of ${result.name}: ${result.revoked ? `${what} was revoked and` : `${what} was`} removed from this computer.${result.warning ? ` (Could not revoke it: ${result.warning} ${revokeHint})` : ""}`;
|
|
2077
|
+
});
|
|
2078
|
+
if (envNote) lines.push(envNote);
|
|
2079
|
+
io.out(lines.join("\n"));
|
|
2080
|
+
return { exitCode: EXIT_OK };
|
|
2081
|
+
}
|
|
2082
|
+
async function runWhoamiCommand(argv, deps = {}) {
|
|
2083
|
+
const io = deps.io ?? processIo;
|
|
2084
|
+
const env = deps.env ?? process.env;
|
|
2085
|
+
const brand = cliBrand(env);
|
|
2086
|
+
const parsed = parseArgs(argv, { values: [...COMMON_VALUE_FLAGS], booleans: [...COMMON_BOOLEAN_FLAGS] });
|
|
2087
|
+
if (!parsed.ok) return { exitCode: EXIT_USAGE, error: `${parsed.error}
|
|
2088
|
+
Usage: ${brand.bin} whoami [--workspace <name>] [--json]` };
|
|
2089
|
+
const { args } = parsed;
|
|
2090
|
+
try {
|
|
2091
|
+
const resolved = await resolveAuth(
|
|
2092
|
+
{ apiKey: args.values["--api-key"], baseUrl: args.values["--base-url"], workspace: args.values["--workspace"] },
|
|
2093
|
+
env
|
|
2094
|
+
);
|
|
2095
|
+
if (!resolved.ok) return { exitCode: EXIT_FAILED, error: resolved.error };
|
|
2096
|
+
const { auth } = resolved;
|
|
2097
|
+
const response = await apiRequest(auth.baseUrl, "/cli/session", { apiKey: auth.apiKey, env });
|
|
2098
|
+
if (!response.ok) {
|
|
2099
|
+
const hint = response.status === 401 ? ` The credential was rejected; run ${brand.bin} login to sign in again.` : "";
|
|
2100
|
+
return { exitCode: EXIT_FAILED, error: `${sentence(errorMessage(response, "Could not check the sign-in"))}${hint}` };
|
|
2101
|
+
}
|
|
2102
|
+
const session = response.body;
|
|
2103
|
+
const source = sourceLabel(auth.source, env);
|
|
2104
|
+
if (wantsJson(args, io)) {
|
|
2105
|
+
printJson(io, { ...response.body, source, api_url: auth.baseUrl, config_path: configPath(env) });
|
|
2106
|
+
return { exitCode: EXIT_OK };
|
|
2107
|
+
}
|
|
2108
|
+
const scope = session.scope ?? (session.key?.scope === "sending" ? "emails:send emails:read" : "full");
|
|
2109
|
+
const credential = session.oauth ? `Signed in with OAuth: ${session.oauth.client_name ?? brand.product}${session.oauth.device_name ? ` on ${session.oauth.device_name}` : ""}, from ${source}` : `Key: ${session.key?.name ?? "?"} (${session.key?.prefix ?? "?"}\u2026), from ${source}`;
|
|
2110
|
+
io.out(
|
|
2111
|
+
[
|
|
2112
|
+
session.user?.email ?? "(an API key; no person is recorded for it)",
|
|
2113
|
+
`Workspace: ${session.workspace?.name ?? "?"} (${session.workspace?.id ?? "?"})`,
|
|
2114
|
+
credential,
|
|
2115
|
+
`Scopes: ${scope === "full" ? "full" : scope.split(" ").join(", ")}`,
|
|
2116
|
+
...session.allowed_domains?.length ? [`Sends only from: ${session.allowed_domains.join(", ")}`] : [],
|
|
2117
|
+
...auth.baseUrl !== defaultApiUrl(env) ? [`API: ${auth.baseUrl}`] : []
|
|
2118
|
+
].join("\n")
|
|
2119
|
+
);
|
|
2120
|
+
return { exitCode: EXIT_OK };
|
|
2121
|
+
} catch (error) {
|
|
2122
|
+
if (error instanceof CliRequestError) return { exitCode: EXIT_FAILED, error: error.message };
|
|
2123
|
+
throw error;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/commands/keys/create.ts
|
|
2128
|
+
function readFlag16(args, flag) {
|
|
2129
|
+
const index = args.indexOf(flag);
|
|
2130
|
+
if (index === -1) return void 0;
|
|
2131
|
+
return args[index + 1];
|
|
2132
|
+
}
|
|
2133
|
+
async function runCreateKeyCommandWithClient(args, env = process.env) {
|
|
2134
|
+
const apiKey = readFlag16(args, "--api-key") || env.GROUNDMAIL_API_KEY;
|
|
2135
|
+
const baseUrl = readFlag16(args, "--base-url") || defaultApiUrl(env);
|
|
2136
|
+
if (!apiKey) {
|
|
2137
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2138
|
+
}
|
|
2139
|
+
const name = readFlag16(args, "--name");
|
|
2140
|
+
const scope = readFlag16(args, "--scope");
|
|
2141
|
+
if (scope && scope !== "full" && scope !== "sending") {
|
|
2142
|
+
return { exitCode: 1, error: "--scope must be full or sending (or use --scopes for fine-grained scopes)" };
|
|
2143
|
+
}
|
|
2144
|
+
const scopesFlag = readFlag16(args, "--scopes");
|
|
2145
|
+
const scopes = scopesFlag !== void 0 ? parseScopeFlag(scopesFlag) : null;
|
|
2146
|
+
if (scopes && !scopes.ok) return { exitCode: 1, error: scopes.error };
|
|
2147
|
+
const domainsFlag = readFlag16(args, "--allowed-domains");
|
|
2148
|
+
const allowedDomains = domainsFlag?.split(/[\s,]+/).map((domain) => domain.trim()).filter(Boolean);
|
|
2149
|
+
try {
|
|
2150
|
+
const response = await apiRequest(baseUrl, "/api-keys", {
|
|
2151
|
+
apiKey,
|
|
2152
|
+
env,
|
|
2153
|
+
body: {
|
|
2154
|
+
...name ? { name } : {},
|
|
2155
|
+
...scope && !scopes ? { scope } : {},
|
|
2156
|
+
...scopes?.ok ? { scopes: scopes.scope.split(" ") } : {},
|
|
2157
|
+
...allowedDomains?.length ? { allowed_domains: allowedDomains } : {}
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
if (!response.ok) {
|
|
2161
|
+
return { exitCode: 1, error: errorMessage(response, "Could not create the key") };
|
|
2162
|
+
}
|
|
2163
|
+
console.log(JSON.stringify(response.body));
|
|
2164
|
+
return { exitCode: 0 };
|
|
2165
|
+
} catch (error) {
|
|
2166
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// src/commands/keys/delete.ts
|
|
2171
|
+
function readFlag17(args, flag) {
|
|
2172
|
+
const index = args.indexOf(flag);
|
|
2173
|
+
if (index === -1) return void 0;
|
|
2174
|
+
return args[index + 1];
|
|
2175
|
+
}
|
|
2176
|
+
async function runDeleteKeyCommandWithClient(args, env = process.env) {
|
|
2177
|
+
const apiKey = readFlag17(args, "--api-key") || env.GROUNDMAIL_API_KEY;
|
|
2178
|
+
const baseUrl = readFlag17(args, "--base-url");
|
|
2179
|
+
const id = readFlag17(args, "--id");
|
|
2180
|
+
if (!apiKey) {
|
|
2181
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2182
|
+
}
|
|
2183
|
+
if (!id) {
|
|
2184
|
+
return { exitCode: 1, error: "Required: --id <api-key-id>" };
|
|
2185
|
+
}
|
|
2186
|
+
const { Groundmail } = await import("groundmail");
|
|
2187
|
+
const client = new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
|
|
2188
|
+
try {
|
|
2189
|
+
const result = await client.apiKeys.remove(id);
|
|
2190
|
+
console.log(JSON.stringify(result));
|
|
2191
|
+
return { exitCode: 0 };
|
|
2192
|
+
} catch (error) {
|
|
2193
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2194
|
+
console.error(message);
|
|
2195
|
+
return { exitCode: 1, error: message };
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/commands/suppressions/shared.ts
|
|
2200
|
+
function readFlag18(args, flag) {
|
|
2201
|
+
const index = args.indexOf(flag);
|
|
2202
|
+
if (index === -1) return void 0;
|
|
2203
|
+
return args[index + 1];
|
|
2204
|
+
}
|
|
2205
|
+
function parseSuppressionsCommonArgs(args) {
|
|
2206
|
+
return {
|
|
2207
|
+
apiKey: readFlag18(args, "--api-key"),
|
|
2208
|
+
baseUrl: readFlag18(args, "--base-url")
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
function resolveApiKey6(options, env) {
|
|
2212
|
+
return options.apiKey || env.GROUNDMAIL_API_KEY || null;
|
|
2213
|
+
}
|
|
2214
|
+
async function createSuppressionsClient(apiKey, baseUrl) {
|
|
2215
|
+
const { Groundmail } = await import("groundmail");
|
|
2216
|
+
return new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// src/commands/suppressions/list.ts
|
|
2220
|
+
async function runListCommand6(args, env = process.env) {
|
|
2221
|
+
const options = parseSuppressionsCommonArgs(args);
|
|
2222
|
+
const apiKey = resolveApiKey6(options, env);
|
|
2223
|
+
if (!apiKey) {
|
|
2224
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2225
|
+
}
|
|
2226
|
+
try {
|
|
2227
|
+
const client = await createSuppressionsClient(apiKey, options.baseUrl);
|
|
2228
|
+
const limitRaw = readFlag18(args, "--limit");
|
|
2229
|
+
const cursor = readFlag18(args, "--cursor");
|
|
2230
|
+
const email = readFlag18(args, "--email");
|
|
2231
|
+
const result = await client.suppressions.list({
|
|
2232
|
+
...limitRaw ? { limit: Number(limitRaw) } : {},
|
|
2233
|
+
...cursor ? { cursor } : {},
|
|
2234
|
+
...email ? { email } : {}
|
|
2235
|
+
});
|
|
2236
|
+
console.log(JSON.stringify(result));
|
|
2237
|
+
return { exitCode: 0 };
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// src/commands/suppressions/add.ts
|
|
2244
|
+
async function runAddCommand2(args, env = process.env) {
|
|
2245
|
+
const options = parseSuppressionsCommonArgs(args);
|
|
2246
|
+
const apiKey = resolveApiKey6(options, env);
|
|
2247
|
+
if (!apiKey) {
|
|
2248
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2249
|
+
}
|
|
2250
|
+
const email = readFlag18(args, "--email") || args.find((a) => a.includes("@") && !a.startsWith("-"));
|
|
2251
|
+
if (!email) {
|
|
2252
|
+
return { exitCode: 1, error: "Missing email. Pass --email user@example.com" };
|
|
2253
|
+
}
|
|
2254
|
+
try {
|
|
2255
|
+
const client = await createSuppressionsClient(apiKey, options.baseUrl);
|
|
2256
|
+
const reason = readFlag18(args, "--reason");
|
|
2257
|
+
const result = await client.suppressions.create({
|
|
2258
|
+
email,
|
|
2259
|
+
...reason ? { reason } : {}
|
|
2260
|
+
});
|
|
2261
|
+
console.log(JSON.stringify(result));
|
|
2262
|
+
return { exitCode: 0 };
|
|
2263
|
+
} catch (error) {
|
|
2264
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// src/commands/suppressions/remove.ts
|
|
2269
|
+
async function runRemoveCommand(args, env = process.env) {
|
|
2270
|
+
const options = parseSuppressionsCommonArgs(args);
|
|
2271
|
+
const apiKey = resolveApiKey6(options, env);
|
|
1833
2272
|
if (!apiKey) {
|
|
1834
2273
|
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
1835
2274
|
}
|
|
1836
|
-
const
|
|
1837
|
-
if (!
|
|
1838
|
-
return { exitCode: 1, error: "Missing
|
|
2275
|
+
const idOrEmail = readFlag18(args, "--id") || readFlag18(args, "--email") || args.find((a) => !a.startsWith("-") && a !== "remove" && a !== "delete");
|
|
2276
|
+
if (!idOrEmail) {
|
|
2277
|
+
return { exitCode: 1, error: "Missing id or email. Pass --id <uuid> or --email user@example.com" };
|
|
1839
2278
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
const unsubscribeEnabled = args.includes("--unsubscribe-enabled");
|
|
1848
|
-
const unsubscribeDisabled = args.includes("--no-unsubscribe-enabled");
|
|
1849
|
-
if (!name && !subject && !html && !text && !alias && variables.length === 0 && !category && !unsubscribeEnabled && !unsubscribeDisabled) {
|
|
1850
|
-
return {
|
|
1851
|
-
exitCode: 1,
|
|
1852
|
-
error: "Nothing to update. Pass --name, --subject, --html, --text, --set-alias, --category, --variable, or unsubscribe flags."
|
|
1853
|
-
};
|
|
2279
|
+
try {
|
|
2280
|
+
const client = await createSuppressionsClient(apiKey, options.baseUrl);
|
|
2281
|
+
const result = await client.suppressions.remove(idOrEmail);
|
|
2282
|
+
console.log(JSON.stringify(result));
|
|
2283
|
+
return { exitCode: 0 };
|
|
2284
|
+
} catch (error) {
|
|
2285
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1854
2286
|
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/commands/templates/shared.ts
|
|
2290
|
+
function readFlag19(args, flag) {
|
|
2291
|
+
const index = args.indexOf(flag);
|
|
2292
|
+
if (index === -1) return void 0;
|
|
2293
|
+
return args[index + 1];
|
|
2294
|
+
}
|
|
2295
|
+
function parseTemplatesCommonArgs(args) {
|
|
2296
|
+
return {
|
|
2297
|
+
apiKey: readFlag19(args, "--api-key"),
|
|
2298
|
+
baseUrl: readFlag19(args, "--base-url")
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
function resolveApiKey7(options, env) {
|
|
2302
|
+
return options.apiKey || env.GROUNDMAIL_API_KEY || null;
|
|
2303
|
+
}
|
|
2304
|
+
async function createTemplatesClient(apiKey, baseUrl) {
|
|
2305
|
+
const { Groundmail } = await import("groundmail");
|
|
2306
|
+
return new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/commands/templates/delete.ts
|
|
2310
|
+
var POSITIONAL_FLAGS7 = ["--api-key", "--base-url", "--id", "--alias"];
|
|
2311
|
+
async function runDeleteCommand4(args, env = process.env) {
|
|
2312
|
+
const options = parseTemplatesCommonArgs(args);
|
|
2313
|
+
const apiKey = resolveApiKey7(options, env);
|
|
2314
|
+
if (!apiKey) return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2315
|
+
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional8(args);
|
|
2316
|
+
if (!idOrAlias) return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
|
|
1855
2317
|
try {
|
|
1856
2318
|
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
1857
|
-
|
|
1858
|
-
...name ? { name } : {},
|
|
1859
|
-
...subject ? { subject } : {},
|
|
1860
|
-
...html ? { html } : {},
|
|
1861
|
-
...text ? { text } : {},
|
|
1862
|
-
...alias ? { alias } : {},
|
|
1863
|
-
...variables.length > 0 ? { variables } : {},
|
|
1864
|
-
...category ? { category } : {},
|
|
1865
|
-
...unsubscribeEnabled ? { unsubscribe_enabled: true } : {},
|
|
1866
|
-
...unsubscribeDisabled ? { unsubscribe_enabled: false } : {}
|
|
1867
|
-
});
|
|
1868
|
-
console.log(JSON.stringify(result));
|
|
2319
|
+
console.log(JSON.stringify(await client.templates.remove(idOrAlias)));
|
|
1869
2320
|
return { exitCode: 0 };
|
|
1870
2321
|
} catch (error) {
|
|
1871
2322
|
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1872
2323
|
}
|
|
1873
2324
|
}
|
|
1874
|
-
function
|
|
1875
|
-
const values = [];
|
|
2325
|
+
function readFirstPositional8(args) {
|
|
1876
2326
|
for (let i = 0; i < args.length; i += 1) {
|
|
1877
|
-
|
|
2327
|
+
const arg = args[i];
|
|
2328
|
+
if (arg.startsWith("--")) {
|
|
2329
|
+
if (POSITIONAL_FLAGS7.includes(arg)) i += 1;
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
return arg;
|
|
1878
2333
|
}
|
|
1879
|
-
return
|
|
2334
|
+
return void 0;
|
|
1880
2335
|
}
|
|
1881
|
-
|
|
2336
|
+
|
|
2337
|
+
// src/commands/templates/duplicate.ts
|
|
2338
|
+
var POSITIONAL_FLAGS8 = ["--api-key", "--base-url", "--id", "--alias", "--name"];
|
|
2339
|
+
async function runDuplicateCommand(args, env = process.env) {
|
|
2340
|
+
const options = parseTemplatesCommonArgs(args);
|
|
2341
|
+
const apiKey = resolveApiKey7(options, env);
|
|
2342
|
+
if (!apiKey) return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2343
|
+
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional9(args);
|
|
2344
|
+
if (!idOrAlias) return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
|
|
2345
|
+
const name = readFlag19(args, "--name");
|
|
2346
|
+
try {
|
|
2347
|
+
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2348
|
+
console.log(JSON.stringify(await client.templates.duplicate(idOrAlias, name ? { name } : {})));
|
|
2349
|
+
return { exitCode: 0 };
|
|
2350
|
+
} catch (error) {
|
|
2351
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
function readFirstPositional9(args) {
|
|
1882
2355
|
for (let i = 0; i < args.length; i += 1) {
|
|
1883
2356
|
const arg = args[i];
|
|
1884
2357
|
if (arg.startsWith("--")) {
|
|
1885
|
-
if (
|
|
2358
|
+
if (POSITIONAL_FLAGS8.includes(arg)) i += 1;
|
|
1886
2359
|
continue;
|
|
1887
2360
|
}
|
|
1888
2361
|
return arg;
|
|
@@ -1890,808 +2363,666 @@ function readFirstPositional12(args) {
|
|
|
1890
2363
|
return void 0;
|
|
1891
2364
|
}
|
|
1892
2365
|
|
|
1893
|
-
// src/commands/templates/
|
|
1894
|
-
var
|
|
1895
|
-
function
|
|
2366
|
+
// src/commands/templates/get.ts
|
|
2367
|
+
var POSITIONAL_FLAGS9 = ["--api-key", "--base-url", "--id", "--alias"];
|
|
2368
|
+
async function runGetCommand6(args, env = process.env) {
|
|
2369
|
+
const options = parseTemplatesCommonArgs(args);
|
|
2370
|
+
const apiKey = resolveApiKey7(options, env);
|
|
2371
|
+
if (!apiKey) return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2372
|
+
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional10(args);
|
|
2373
|
+
if (!idOrAlias) return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
|
|
2374
|
+
try {
|
|
2375
|
+
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2376
|
+
console.log(JSON.stringify(await client.templates.get(idOrAlias)));
|
|
2377
|
+
return { exitCode: 0 };
|
|
2378
|
+
} catch (error) {
|
|
2379
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
function readFirstPositional10(args) {
|
|
1896
2383
|
for (let i = 0; i < args.length; i += 1) {
|
|
1897
2384
|
const arg = args[i];
|
|
1898
2385
|
if (arg.startsWith("--")) {
|
|
1899
|
-
if (
|
|
2386
|
+
if (POSITIONAL_FLAGS9.includes(arg)) i += 1;
|
|
1900
2387
|
continue;
|
|
1901
2388
|
}
|
|
1902
2389
|
return arg;
|
|
1903
2390
|
}
|
|
1904
2391
|
return void 0;
|
|
1905
2392
|
}
|
|
1906
|
-
|
|
2393
|
+
|
|
2394
|
+
// src/commands/templates/list.ts
|
|
2395
|
+
async function runListCommand7(args, env = process.env) {
|
|
1907
2396
|
const options = parseTemplatesCommonArgs(args);
|
|
1908
2397
|
const apiKey = resolveApiKey7(options, env);
|
|
1909
2398
|
if (!apiKey) {
|
|
1910
2399
|
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
1911
2400
|
}
|
|
1912
|
-
const alias = readFlag19(args, "--alias") || readFirstPositional13(args);
|
|
1913
|
-
if (!alias) {
|
|
1914
|
-
return {
|
|
1915
|
-
exitCode: 1,
|
|
1916
|
-
error: "Missing starter alias. Pass password-reset (or --alias welcome). List: groundmail templates starters"
|
|
1917
|
-
};
|
|
1918
|
-
}
|
|
1919
2401
|
try {
|
|
1920
2402
|
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
1921
|
-
|
|
2403
|
+
const limitRaw = readFlag19(args, "--limit");
|
|
2404
|
+
const cursor = readFlag19(args, "--cursor");
|
|
2405
|
+
const status = readFlag19(args, "--status");
|
|
2406
|
+
const result = await client.templates.list({
|
|
2407
|
+
...limitRaw ? { limit: Number(limitRaw) } : {},
|
|
2408
|
+
...cursor ? { cursor } : {},
|
|
2409
|
+
...status ? { status } : {}
|
|
2410
|
+
});
|
|
2411
|
+
console.log(JSON.stringify(result));
|
|
1922
2412
|
return { exitCode: 0 };
|
|
1923
2413
|
} catch (error) {
|
|
1924
2414
|
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1925
2415
|
}
|
|
1926
2416
|
}
|
|
1927
|
-
|
|
2417
|
+
|
|
2418
|
+
// src/commands/templates/publish.ts
|
|
2419
|
+
var POSITIONAL_FLAGS10 = ["--api-key", "--base-url", "--id", "--alias"];
|
|
2420
|
+
async function runPublishCommand(args, env = process.env) {
|
|
1928
2421
|
const options = parseTemplatesCommonArgs(args);
|
|
1929
2422
|
const apiKey = resolveApiKey7(options, env);
|
|
1930
|
-
if (!apiKey) {
|
|
1931
|
-
|
|
2423
|
+
if (!apiKey) return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2424
|
+
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional11(args);
|
|
2425
|
+
if (!idOrAlias) return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
|
|
2426
|
+
try {
|
|
2427
|
+
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2428
|
+
console.log(JSON.stringify(await client.templates.publish(idOrAlias)));
|
|
2429
|
+
return { exitCode: 0 };
|
|
2430
|
+
} catch (error) {
|
|
2431
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function readFirstPositional11(args) {
|
|
2435
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
2436
|
+
const arg = args[i];
|
|
2437
|
+
if (arg.startsWith("--")) {
|
|
2438
|
+
if (POSITIONAL_FLAGS10.includes(arg)) i += 1;
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
return arg;
|
|
2442
|
+
}
|
|
2443
|
+
return void 0;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// src/commands/templates/render-source.ts
|
|
2447
|
+
var HTML_EXTENSIONS = [".html", ".htm"];
|
|
2448
|
+
var TEXT_EXTENSIONS = [".txt", ".text"];
|
|
2449
|
+
var MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx"];
|
|
2450
|
+
var TYPESCRIPT_EXTENSIONS = [".ts", ".tsx", ".jsx"];
|
|
2451
|
+
function extensionOf(filePath) {
|
|
2452
|
+
const match = /\.[^.\/\\]+$/.exec(filePath);
|
|
2453
|
+
return match ? match[0].toLowerCase() : "";
|
|
2454
|
+
}
|
|
2455
|
+
async function defaultReadFile(filePath) {
|
|
2456
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
2457
|
+
return readFile3(filePath, "utf8");
|
|
2458
|
+
}
|
|
2459
|
+
async function defaultImportModule(filePath) {
|
|
2460
|
+
const { pathToFileURL } = await import("url");
|
|
2461
|
+
const { resolve } = await import("path");
|
|
2462
|
+
const url = pathToFileURL(resolve(filePath)).href;
|
|
2463
|
+
try {
|
|
2464
|
+
return await import(url);
|
|
2465
|
+
} catch (error) {
|
|
2466
|
+
if (!TYPESCRIPT_EXTENSIONS.includes(extensionOf(filePath))) throw error;
|
|
2467
|
+
try {
|
|
2468
|
+
const specifier = "tsx/esm/api";
|
|
2469
|
+
const tsx = await import(specifier);
|
|
2470
|
+
tsx.register();
|
|
2471
|
+
} catch {
|
|
2472
|
+
throw new Error(
|
|
2473
|
+
`Cannot import ${filePath} directly. Install tsx to push TypeScript or JSX templates (npm install -D tsx), or pre-render the file to .html.`
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
return import(url);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
async function loadReactEmailRender() {
|
|
2480
|
+
try {
|
|
2481
|
+
const mod = await import("@react-email/render");
|
|
2482
|
+
const render = mod.render ?? mod.default?.render;
|
|
2483
|
+
if (typeof render !== "function") throw new Error("missing render export");
|
|
2484
|
+
return render;
|
|
2485
|
+
} catch {
|
|
2486
|
+
throw new Error(
|
|
2487
|
+
"Rendering a React email requires @react-email/render. Install it with: npm install @react-email/render react"
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
async function toElement(mod, props) {
|
|
2492
|
+
const record = mod;
|
|
2493
|
+
const exported = record?.default ?? mod;
|
|
2494
|
+
if (typeof exported === "function") {
|
|
2495
|
+
const react = await import("./react-IXBRBSFN.js");
|
|
2496
|
+
const createElement = react.createElement ?? react.default?.createElement;
|
|
2497
|
+
if (typeof createElement !== "function") {
|
|
2498
|
+
throw new Error("Rendering a React email requires react. Install it with: npm install react");
|
|
2499
|
+
}
|
|
2500
|
+
return createElement(exported, props);
|
|
2501
|
+
}
|
|
2502
|
+
if (!exported) {
|
|
2503
|
+
throw new Error("The template module must export a React component as its default export.");
|
|
2504
|
+
}
|
|
2505
|
+
return exported;
|
|
2506
|
+
}
|
|
2507
|
+
async function loadTemplateSource(filePath, options = {}, deps = {}) {
|
|
2508
|
+
const readFile3 = deps.readFile ?? defaultReadFile;
|
|
2509
|
+
const importModule = deps.importModule ?? defaultImportModule;
|
|
2510
|
+
const extension = extensionOf(filePath);
|
|
2511
|
+
if (HTML_EXTENSIONS.includes(extension)) {
|
|
2512
|
+
return { html: await readFile3(filePath) };
|
|
2513
|
+
}
|
|
2514
|
+
if (TEXT_EXTENSIONS.includes(extension)) {
|
|
2515
|
+
return { text: await readFile3(filePath) };
|
|
2516
|
+
}
|
|
2517
|
+
if (!MODULE_EXTENSIONS.includes(extension)) {
|
|
2518
|
+
throw new Error(
|
|
2519
|
+
`Unsupported template file "${filePath}". Use .html, .txt, or a module exporting a React component (${MODULE_EXTENSIONS.join(", ")}).`
|
|
2520
|
+
);
|
|
1932
2521
|
}
|
|
2522
|
+
const mod = await importModule(filePath);
|
|
2523
|
+
const element = await toElement(mod, options.props ?? {});
|
|
2524
|
+
const render = await loadReactEmailRender();
|
|
2525
|
+
const html = await render(element);
|
|
2526
|
+
let text;
|
|
1933
2527
|
try {
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
} catch (error) {
|
|
1938
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2528
|
+
text = await render(element, { plainText: true });
|
|
2529
|
+
} catch {
|
|
2530
|
+
text = void 0;
|
|
1939
2531
|
}
|
|
2532
|
+
return { html, ...text ? { text } : {} };
|
|
1940
2533
|
}
|
|
1941
2534
|
|
|
1942
|
-
// src/commands/templates/
|
|
1943
|
-
var
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2535
|
+
// src/commands/templates/push.ts
|
|
2536
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
2537
|
+
"--alias",
|
|
2538
|
+
"--subject",
|
|
2539
|
+
"--name",
|
|
2540
|
+
"--text",
|
|
2541
|
+
"--preheader",
|
|
2542
|
+
"--props",
|
|
2543
|
+
"--api-key",
|
|
2544
|
+
"--base-url"
|
|
2545
|
+
]);
|
|
2546
|
+
function readPositional(args) {
|
|
2547
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2548
|
+
const arg = args[index];
|
|
1947
2549
|
if (arg.startsWith("--")) {
|
|
1948
|
-
if (
|
|
2550
|
+
if (VALUE_FLAGS.has(arg)) index += 1;
|
|
1949
2551
|
continue;
|
|
1950
2552
|
}
|
|
1951
2553
|
return arg;
|
|
1952
2554
|
}
|
|
1953
2555
|
return void 0;
|
|
1954
2556
|
}
|
|
1955
|
-
|
|
1956
|
-
const
|
|
1957
|
-
|
|
1958
|
-
if (!apiKey) {
|
|
1959
|
-
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
1960
|
-
}
|
|
1961
|
-
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional14(args);
|
|
1962
|
-
if (!idOrAlias) {
|
|
1963
|
-
return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
|
|
1964
|
-
}
|
|
1965
|
-
try {
|
|
1966
|
-
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
1967
|
-
console.log(JSON.stringify(await client.templates.listVersions(idOrAlias)));
|
|
1968
|
-
return { exitCode: 0 };
|
|
1969
|
-
} catch (error) {
|
|
1970
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
async function runRestoreCommand(args, env = process.env) {
|
|
1974
|
-
const options = parseTemplatesCommonArgs(args);
|
|
1975
|
-
const apiKey = resolveApiKey7(options, env);
|
|
1976
|
-
if (!apiKey) {
|
|
1977
|
-
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
1978
|
-
}
|
|
1979
|
-
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional14(args);
|
|
1980
|
-
const versionRaw = readFlag19(args, "--version");
|
|
1981
|
-
const version2 = versionRaw ? Number.parseInt(versionRaw, 10) : NaN;
|
|
1982
|
-
if (!idOrAlias || !Number.isFinite(version2) || version2 < 1) {
|
|
1983
|
-
return {
|
|
1984
|
-
exitCode: 1,
|
|
1985
|
-
error: "Usage: groundmail templates restore --id <id|alias> --version <n>"
|
|
1986
|
-
};
|
|
1987
|
-
}
|
|
1988
|
-
try {
|
|
1989
|
-
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
1990
|
-
console.log(JSON.stringify(await client.templates.restoreVersion(idOrAlias, version2)));
|
|
1991
|
-
return { exitCode: 0 };
|
|
1992
|
-
} catch (error) {
|
|
1993
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1994
|
-
}
|
|
2557
|
+
function statusOf(error) {
|
|
2558
|
+
const status = error?.status;
|
|
2559
|
+
return typeof status === "number" ? status : void 0;
|
|
1995
2560
|
}
|
|
1996
|
-
async function
|
|
2561
|
+
async function runPushCommand(args, env = process.env, deps = {}) {
|
|
1997
2562
|
const options = parseTemplatesCommonArgs(args);
|
|
1998
2563
|
const apiKey = resolveApiKey7(options, env);
|
|
1999
2564
|
if (!apiKey) {
|
|
2000
2565
|
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2001
2566
|
}
|
|
2002
|
-
const
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
if (!idOrAlias || !from || !to) {
|
|
2006
|
-
return {
|
|
2007
|
-
exitCode: 1,
|
|
2008
|
-
error: "Usage: groundmail templates test-send --id <id|alias> --from <email> --to <email>"
|
|
2009
|
-
};
|
|
2010
|
-
}
|
|
2011
|
-
try {
|
|
2012
|
-
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2013
|
-
console.log(JSON.stringify(await client.templates.testSend(idOrAlias, { from, to })));
|
|
2014
|
-
return { exitCode: 0 };
|
|
2015
|
-
} catch (error) {
|
|
2016
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
// src/commands/integrations/shared.ts
|
|
2021
|
-
function readFlag20(args, flag) {
|
|
2022
|
-
const index = args.indexOf(flag);
|
|
2023
|
-
if (index === -1) return void 0;
|
|
2024
|
-
return args[index + 1];
|
|
2025
|
-
}
|
|
2026
|
-
function parseIntegrationsCommonArgs(args) {
|
|
2027
|
-
return {
|
|
2028
|
-
apiKey: readFlag20(args, "--api-key"),
|
|
2029
|
-
baseUrl: readFlag20(args, "--base-url")
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
function resolveApiKey8(options, env) {
|
|
2033
|
-
return options.apiKey || env.GROUNDMAIL_API_KEY || null;
|
|
2034
|
-
}
|
|
2035
|
-
async function createIntegrationsClient(apiKey, baseUrl) {
|
|
2036
|
-
const { Groundmail } = await import("groundmail");
|
|
2037
|
-
return new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
// src/commands/integrations/twilio.ts
|
|
2041
|
-
async function runTwilioStatusCommand(args, env = process.env) {
|
|
2042
|
-
const options = parseIntegrationsCommonArgs(args);
|
|
2043
|
-
const apiKey = resolveApiKey8(options, env);
|
|
2044
|
-
if (!apiKey) {
|
|
2045
|
-
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2046
|
-
}
|
|
2047
|
-
try {
|
|
2048
|
-
const client = await createIntegrationsClient(apiKey, options.baseUrl);
|
|
2049
|
-
const result = await client.integrations.getTwilio();
|
|
2050
|
-
console.log(JSON.stringify(result));
|
|
2051
|
-
return { exitCode: 0 };
|
|
2052
|
-
} catch (error) {
|
|
2053
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
async function runTwilioConnectCommand(args, env = process.env) {
|
|
2057
|
-
const options = parseIntegrationsCommonArgs(args);
|
|
2058
|
-
const apiKey = resolveApiKey8(options, env);
|
|
2059
|
-
if (!apiKey) {
|
|
2060
|
-
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2061
|
-
}
|
|
2062
|
-
const accountSid = readFlag20(args, "--account-sid") || env.TWILIO_ACCOUNT_SID || env.TWILIO_SID || "";
|
|
2063
|
-
const authToken = readFlag20(args, "--auth-token") || env.TWILIO_AUTH_TOKEN || env.TWILIO_TOKEN || "";
|
|
2064
|
-
const defaultFrom = readFlag20(args, "--from") || readFlag20(args, "--default-from") || env.TWILIO_DEFAULT_FROM || "";
|
|
2065
|
-
const messagingServiceSid = readFlag20(args, "--messaging-service-sid") || readFlag20(args, "--messaging-sid") || env.TWILIO_MESSAGING_SERVICE_SID || "";
|
|
2066
|
-
if (!accountSid.trim() || !authToken.trim()) {
|
|
2067
|
-
return {
|
|
2068
|
-
exitCode: 1,
|
|
2069
|
-
error: "Required: --account-sid and --auth-token (or TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN)."
|
|
2070
|
-
};
|
|
2071
|
-
}
|
|
2072
|
-
if (!defaultFrom.trim() && !messagingServiceSid.trim()) {
|
|
2073
|
-
return {
|
|
2074
|
-
exitCode: 1,
|
|
2075
|
-
error: "Required: --from or --messaging-service-sid (or TWILIO_DEFAULT_FROM / TWILIO_MESSAGING_SERVICE_SID)."
|
|
2076
|
-
};
|
|
2567
|
+
const file = readFlag19(args, "--file") ?? readPositional(args);
|
|
2568
|
+
if (!file) {
|
|
2569
|
+
return { exitCode: 1, error: "Missing template file. Usage: templates push <file> --alias <alias>" };
|
|
2077
2570
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
accountSid: accountSid.trim(),
|
|
2082
|
-
authToken: authToken.trim(),
|
|
2083
|
-
...defaultFrom.trim() ? { defaultFrom: defaultFrom.trim() } : {},
|
|
2084
|
-
...messagingServiceSid.trim() ? { messagingServiceSid: messagingServiceSid.trim() } : {}
|
|
2085
|
-
});
|
|
2086
|
-
console.log(JSON.stringify(result));
|
|
2087
|
-
return { exitCode: 0 };
|
|
2088
|
-
} catch (error) {
|
|
2089
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2571
|
+
const alias = readFlag19(args, "--alias");
|
|
2572
|
+
if (!alias) {
|
|
2573
|
+
return { exitCode: 1, error: "Missing --alias. The alias identifies which template to update." };
|
|
2090
2574
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
const
|
|
2094
|
-
const
|
|
2095
|
-
|
|
2096
|
-
|
|
2575
|
+
const subject = readFlag19(args, "--subject");
|
|
2576
|
+
const name = readFlag19(args, "--name");
|
|
2577
|
+
const preheader = readFlag19(args, "--preheader");
|
|
2578
|
+
const textFile = readFlag19(args, "--text");
|
|
2579
|
+
const propsRaw = readFlag19(args, "--props");
|
|
2580
|
+
const shouldPublish = args.includes("--publish");
|
|
2581
|
+
let props = {};
|
|
2582
|
+
if (propsRaw) {
|
|
2583
|
+
try {
|
|
2584
|
+
const parsed = JSON.parse(propsRaw);
|
|
2585
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2586
|
+
throw new Error("not an object");
|
|
2587
|
+
}
|
|
2588
|
+
props = parsed;
|
|
2589
|
+
} catch {
|
|
2590
|
+
return { exitCode: 1, error: `--props must be a JSON object, e.g. --props '{"name":"Ada"}'` };
|
|
2591
|
+
}
|
|
2097
2592
|
}
|
|
2098
2593
|
try {
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
} catch (error) {
|
|
2104
|
-
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
// src/commands/login.ts
|
|
2109
|
-
import { spawn } from "child_process";
|
|
2110
|
-
|
|
2111
|
-
// src/session/args.ts
|
|
2112
|
-
var COMMON_VALUE_FLAGS = ["--api-key", "--base-url", "--workspace"];
|
|
2113
|
-
var COMMON_BOOLEAN_FLAGS = ["--json"];
|
|
2114
|
-
function parseArgs(argv, spec) {
|
|
2115
|
-
const valueFlags = new Set(spec.values ?? []);
|
|
2116
|
-
const booleanFlags = new Set(spec.booleans ?? []);
|
|
2117
|
-
const values = {};
|
|
2118
|
-
const booleans = {};
|
|
2119
|
-
const positionals = [];
|
|
2120
|
-
for (let i = 0; i < argv.length; i++) {
|
|
2121
|
-
const arg = argv[i];
|
|
2122
|
-
if (arg === "--") {
|
|
2123
|
-
positionals.push(...argv.slice(i + 1));
|
|
2124
|
-
break;
|
|
2594
|
+
const source = await loadTemplateSource(file, { props }, deps);
|
|
2595
|
+
if (textFile) {
|
|
2596
|
+
const textSource = await loadTemplateSource(textFile, { props }, deps);
|
|
2597
|
+
source.text = textSource.text ?? textSource.html;
|
|
2125
2598
|
}
|
|
2126
|
-
if (!
|
|
2127
|
-
|
|
2128
|
-
continue;
|
|
2599
|
+
if (!source.html && !source.text) {
|
|
2600
|
+
return { exitCode: 1, error: `${file} produced no content to push.` };
|
|
2129
2601
|
}
|
|
2130
|
-
const
|
|
2131
|
-
const
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2602
|
+
const createClient2 = deps.createClient ?? createTemplatesClient;
|
|
2603
|
+
const client = await createClient2(apiKey, options.baseUrl);
|
|
2604
|
+
let existed = true;
|
|
2605
|
+
try {
|
|
2606
|
+
await client.templates.get(alias);
|
|
2607
|
+
} catch (error) {
|
|
2608
|
+
if (statusOf(error) !== 404) throw error;
|
|
2609
|
+
existed = false;
|
|
2610
|
+
}
|
|
2611
|
+
const body = {
|
|
2612
|
+
format: "html",
|
|
2613
|
+
...source.html ? { html: source.html } : {},
|
|
2614
|
+
...source.text ? { text: source.text } : {},
|
|
2615
|
+
...subject ? { subject } : {},
|
|
2616
|
+
...preheader ? { preheader } : {},
|
|
2617
|
+
...name ? { name } : {}
|
|
2618
|
+
};
|
|
2619
|
+
let result;
|
|
2620
|
+
if (existed) {
|
|
2621
|
+
result = await client.templates.update(alias, body);
|
|
2622
|
+
} else {
|
|
2623
|
+
if (!subject) {
|
|
2624
|
+
return {
|
|
2625
|
+
exitCode: 1,
|
|
2626
|
+
error: `Template "${alias}" does not exist yet. Pass --subject to create it.`
|
|
2627
|
+
};
|
|
2136
2628
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
continue;
|
|
2629
|
+
result = await client.templates.create({
|
|
2630
|
+
name: name || alias,
|
|
2631
|
+
subject,
|
|
2632
|
+
alias,
|
|
2633
|
+
...body
|
|
2634
|
+
});
|
|
2144
2635
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
booleans[negated] = false;
|
|
2148
|
-
continue;
|
|
2636
|
+
if (shouldPublish) {
|
|
2637
|
+
result = await client.templates.publish(alias);
|
|
2149
2638
|
}
|
|
2150
|
-
|
|
2151
|
-
return {
|
|
2639
|
+
console.log(JSON.stringify({ ...result, pushed: { file, alias, created: !existed } }));
|
|
2640
|
+
return { exitCode: 0 };
|
|
2641
|
+
} catch (error) {
|
|
2642
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2152
2643
|
}
|
|
2153
|
-
return { ok: true, args: { values, booleans, positionals } };
|
|
2154
|
-
}
|
|
2155
|
-
function wantsJson(args, stdout = process.stdout) {
|
|
2156
|
-
if (args.booleans["--json"] !== void 0) return args.booleans["--json"];
|
|
2157
|
-
return !stdout.isTTY;
|
|
2158
|
-
}
|
|
2159
|
-
var EXIT_OK = 0;
|
|
2160
|
-
var EXIT_FAILED = 1;
|
|
2161
|
-
var EXIT_USAGE = 2;
|
|
2162
|
-
|
|
2163
|
-
// src/session/brand.ts
|
|
2164
|
-
var GROUNDMAIL = {
|
|
2165
|
-
bin: "groundmail",
|
|
2166
|
-
product: "Groundmail",
|
|
2167
|
-
configDirName: "groundmail",
|
|
2168
|
-
keyEnvVar: "GROUNDMAIL_API_KEY",
|
|
2169
|
-
npx: "npx -y groundmail-cli@latest"
|
|
2170
|
-
};
|
|
2171
|
-
function cliBrand(_env = process.env) {
|
|
2172
|
-
return GROUNDMAIL;
|
|
2173
|
-
}
|
|
2174
|
-
var SDK_DEFAULT_API_URL = "https://api.groundmail.dev";
|
|
2175
|
-
var DEFAULT_API_URL_ENV = "GROUNDMAIL_DEFAULT_API_URL";
|
|
2176
|
-
function defaultApiUrl(env = process.env) {
|
|
2177
|
-
return trimUrl(env[DEFAULT_API_URL_ENV]) || SDK_DEFAULT_API_URL;
|
|
2178
|
-
}
|
|
2179
|
-
function explicitApiUrl(env = process.env) {
|
|
2180
|
-
return trimUrl(env.GROUNDMAIL_API_URL) || null;
|
|
2181
|
-
}
|
|
2182
|
-
function trimUrl(value) {
|
|
2183
|
-
return (value ?? "").trim().replace(/\/+$/, "");
|
|
2184
2644
|
}
|
|
2185
2645
|
|
|
2186
|
-
// src/
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
return {
|
|
2211
|
-
...typeof record.current === "string" && workspaces[record.current] ? { current: record.current } : {},
|
|
2212
|
-
workspaces,
|
|
2213
|
-
...record.pendingLogin && typeof record.pendingLogin.deviceCode === "string" ? { pendingLogin: record.pendingLogin } : {}
|
|
2214
|
-
};
|
|
2215
|
-
} catch {
|
|
2216
|
-
return {};
|
|
2646
|
+
// src/commands/templates/create.ts
|
|
2647
|
+
async function runCreateCommand3(args, env = process.env) {
|
|
2648
|
+
const options = parseTemplatesCommonArgs(args);
|
|
2649
|
+
const apiKey = resolveApiKey7(options, env);
|
|
2650
|
+
if (!apiKey) {
|
|
2651
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2652
|
+
}
|
|
2653
|
+
const name = readFlag19(args, "--name");
|
|
2654
|
+
const subject = readFlag19(args, "--subject");
|
|
2655
|
+
const html = readFlag19(args, "--html");
|
|
2656
|
+
const text = readFlag19(args, "--text");
|
|
2657
|
+
const alias = readFlag19(args, "--alias");
|
|
2658
|
+
const category = readFlag19(args, "--category");
|
|
2659
|
+
const unsubscribeEnabled = args.includes("--unsubscribe-enabled");
|
|
2660
|
+
if (!name) {
|
|
2661
|
+
return { exitCode: 1, error: 'Missing name. Pass --name "Welcome"' };
|
|
2662
|
+
}
|
|
2663
|
+
if (!subject) {
|
|
2664
|
+
return { exitCode: 1, error: 'Missing subject. Pass --subject "Hello {{name}}"' };
|
|
2665
|
+
}
|
|
2666
|
+
if (!html && !text) {
|
|
2667
|
+
return { exitCode: 1, error: 'Missing body. Pass --html "<p>Hi</p>" or --text "Hi"' };
|
|
2217
2668
|
}
|
|
2218
|
-
}
|
|
2219
|
-
function saveConfig(config, env = process.env) {
|
|
2220
|
-
const file = configPath(env);
|
|
2221
|
-
mkdirSync(dirname(file), { recursive: true, mode: 448 });
|
|
2222
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
2223
|
-
writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
2224
|
-
`, { mode: 384 });
|
|
2225
|
-
renameSync(tmp, file);
|
|
2226
2669
|
try {
|
|
2227
|
-
|
|
2228
|
-
|
|
2670
|
+
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2671
|
+
const result = await client.templates.create({
|
|
2672
|
+
name,
|
|
2673
|
+
subject,
|
|
2674
|
+
...html ? { html } : {},
|
|
2675
|
+
...text ? { text } : {},
|
|
2676
|
+
...alias ? { alias } : {},
|
|
2677
|
+
...category ? { category } : {},
|
|
2678
|
+
...unsubscribeEnabled ? { unsubscribe_enabled: true } : {}
|
|
2679
|
+
});
|
|
2680
|
+
console.log(JSON.stringify(result));
|
|
2681
|
+
return { exitCode: 0 };
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2229
2684
|
}
|
|
2230
2685
|
}
|
|
2231
|
-
function updateConfig(change, env = process.env) {
|
|
2232
|
-
const next = change(loadConfig(env));
|
|
2233
|
-
saveConfig(next, env);
|
|
2234
|
-
return next;
|
|
2235
|
-
}
|
|
2236
|
-
function savedWorkspaces(config) {
|
|
2237
|
-
return Object.values(config.workspaces ?? {}).sort((a, b) => a.name.localeCompare(b.name));
|
|
2238
|
-
}
|
|
2239
|
-
function findSavedWorkspace(config, wanted) {
|
|
2240
|
-
const all = savedWorkspaces(config);
|
|
2241
|
-
const key = wanted.trim();
|
|
2242
|
-
const byId = all.find((workspace) => workspace.id === key);
|
|
2243
|
-
if (byId) return { ok: true, workspace: byId };
|
|
2244
|
-
const byName = all.filter((workspace) => workspace.name.trim().toLowerCase() === key.toLowerCase());
|
|
2245
|
-
if (byName.length === 1) return { ok: true, workspace: byName[0] };
|
|
2246
|
-
return { ok: false, reason: byName.length ? "ambiguous" : "none", matches: byName };
|
|
2247
|
-
}
|
|
2248
2686
|
|
|
2249
|
-
// src/
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2687
|
+
// src/commands/templates/update.ts
|
|
2688
|
+
var POSITIONAL_FLAGS11 = [
|
|
2689
|
+
"--api-key",
|
|
2690
|
+
"--base-url",
|
|
2691
|
+
"--id",
|
|
2692
|
+
"--alias",
|
|
2693
|
+
"--set-alias",
|
|
2694
|
+
"--name",
|
|
2695
|
+
"--subject",
|
|
2696
|
+
"--html",
|
|
2697
|
+
"--text",
|
|
2698
|
+
"--variable"
|
|
2699
|
+
];
|
|
2700
|
+
async function runUpdateCommand4(args, env = process.env) {
|
|
2701
|
+
const options = parseTemplatesCommonArgs(args);
|
|
2702
|
+
const apiKey = resolveApiKey7(options, env);
|
|
2703
|
+
if (!apiKey) {
|
|
2704
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2258
2705
|
}
|
|
2259
|
-
|
|
2260
|
-
if (
|
|
2261
|
-
return {
|
|
2262
|
-
ok: false,
|
|
2263
|
-
reason: "none",
|
|
2264
|
-
error: `Not signed in. Run: ${brand.bin} login (it prints a link to approve in the browser). Or set ${brand.keyEnvVar}.`
|
|
2265
|
-
};
|
|
2706
|
+
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional12(args);
|
|
2707
|
+
if (!idOrAlias) {
|
|
2708
|
+
return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
|
|
2266
2709
|
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
const
|
|
2275
|
-
const
|
|
2276
|
-
|
|
2277
|
-
const flagKey = flags.apiKey?.trim();
|
|
2278
|
-
if (flagKey) return { ok: true, auth: { apiKey: flagKey, baseUrl: urlFor(null), source: "flag", workspace: null } };
|
|
2279
|
-
if (flags.workspace?.trim()) {
|
|
2280
|
-
const match = findSavedWorkspace(config, flags.workspace);
|
|
2281
|
-
if (!match.ok) {
|
|
2282
|
-
const saved = savedWorkspaces(config);
|
|
2283
|
-
return {
|
|
2284
|
-
ok: false,
|
|
2285
|
-
error: match.reason === "ambiguous" ? `More than one saved workspace is called "${flags.workspace}". Use its id: ${match.matches.map((w) => w.id).join(", ")}.` : `Not signed in to "${flags.workspace}" on this computer.${saved.length ? ` Signed in: ${saved.map((w) => `"${w.name}"`).join(", ")}.` : ""} To add it: ${brand.bin} login --workspace "${flags.workspace}".`
|
|
2286
|
-
};
|
|
2287
|
-
}
|
|
2710
|
+
const name = readFlag19(args, "--name");
|
|
2711
|
+
const subject = readFlag19(args, "--subject");
|
|
2712
|
+
const html = readFlag19(args, "--html");
|
|
2713
|
+
const text = readFlag19(args, "--text");
|
|
2714
|
+
const alias = readFlag19(args, "--set-alias");
|
|
2715
|
+
const variables = readFlags3(args, "--variable");
|
|
2716
|
+
const category = readFlag19(args, "--category");
|
|
2717
|
+
const unsubscribeEnabled = args.includes("--unsubscribe-enabled");
|
|
2718
|
+
const unsubscribeDisabled = args.includes("--no-unsubscribe-enabled");
|
|
2719
|
+
if (!name && !subject && !html && !text && !alias && variables.length === 0 && !category && !unsubscribeEnabled && !unsubscribeDisabled) {
|
|
2288
2720
|
return {
|
|
2289
|
-
|
|
2290
|
-
|
|
2721
|
+
exitCode: 1,
|
|
2722
|
+
error: "Nothing to update. Pass --name, --subject, --html, --text, --set-alias, --category, --variable, or unsubscribe flags."
|
|
2291
2723
|
};
|
|
2292
2724
|
}
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2725
|
+
try {
|
|
2726
|
+
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2727
|
+
const result = await client.templates.update(idOrAlias, {
|
|
2728
|
+
...name ? { name } : {},
|
|
2729
|
+
...subject ? { subject } : {},
|
|
2730
|
+
...html ? { html } : {},
|
|
2731
|
+
...text ? { text } : {},
|
|
2732
|
+
...alias ? { alias } : {},
|
|
2733
|
+
...variables.length > 0 ? { variables } : {},
|
|
2734
|
+
...category ? { category } : {},
|
|
2735
|
+
...unsubscribeEnabled ? { unsubscribe_enabled: true } : {},
|
|
2736
|
+
...unsubscribeDisabled ? { unsubscribe_enabled: false } : {}
|
|
2737
|
+
});
|
|
2738
|
+
console.log(JSON.stringify(result));
|
|
2739
|
+
return { exitCode: 0 };
|
|
2740
|
+
} catch (error) {
|
|
2741
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2742
|
+
}
|
|
2301
2743
|
}
|
|
2302
|
-
function
|
|
2303
|
-
const
|
|
2304
|
-
let
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2744
|
+
function readFlags3(args, flag) {
|
|
2745
|
+
const values = [];
|
|
2746
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
2747
|
+
if (args[i] === flag && args[i + 1]) values.push(args[i + 1]);
|
|
2748
|
+
}
|
|
2749
|
+
return values;
|
|
2750
|
+
}
|
|
2751
|
+
function readFirstPositional12(args) {
|
|
2752
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
2753
|
+
const arg = args[i];
|
|
2754
|
+
if (arg.startsWith("--")) {
|
|
2755
|
+
if (POSITIONAL_FLAGS11.includes(arg)) i += 1;
|
|
2756
|
+
continue;
|
|
2313
2757
|
}
|
|
2758
|
+
return arg;
|
|
2314
2759
|
}
|
|
2315
|
-
|
|
2316
|
-
const url = explicitApiUrl(env) || trimUrl(workspace?.apiUrl) || trimUrl(env.GROUNDMAIL_DEFAULT_API_URL);
|
|
2317
|
-
return { argv: url ? [...argv, "--base-url", url] : argv, notice };
|
|
2760
|
+
return void 0;
|
|
2318
2761
|
}
|
|
2319
2762
|
|
|
2320
|
-
//
|
|
2321
|
-
var
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2763
|
+
// src/commands/templates/from-starter.ts
|
|
2764
|
+
var POSITIONAL_FLAGS12 = ["--api-key", "--base-url", "--alias"];
|
|
2765
|
+
function readFirstPositional13(args) {
|
|
2766
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
2767
|
+
const arg = args[i];
|
|
2768
|
+
if (arg.startsWith("--")) {
|
|
2769
|
+
if (POSITIONAL_FLAGS12.includes(arg)) i += 1;
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
return arg;
|
|
2773
|
+
}
|
|
2774
|
+
return void 0;
|
|
2326
2775
|
}
|
|
2327
|
-
function
|
|
2328
|
-
const
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
return { ...init, headers: next };
|
|
2776
|
+
async function runFromStarterCommand(args, env = process.env) {
|
|
2777
|
+
const options = parseTemplatesCommonArgs(args);
|
|
2778
|
+
const apiKey = resolveApiKey7(options, env);
|
|
2779
|
+
if (!apiKey) {
|
|
2780
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2333
2781
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
return {
|
|
2782
|
+
const alias = readFlag19(args, "--alias") || readFirstPositional13(args);
|
|
2783
|
+
if (!alias) {
|
|
2784
|
+
return {
|
|
2785
|
+
exitCode: 1,
|
|
2786
|
+
error: "Missing starter alias. Pass password-reset (or --alias welcome). List: groundmail templates starters"
|
|
2787
|
+
};
|
|
2337
2788
|
}
|
|
2338
|
-
const record = headers ?? {};
|
|
2339
|
-
const has = Object.keys(record).some((key) => key.toLowerCase() === "user-agent");
|
|
2340
|
-
return { ...init, headers: has ? record : { ...record, "User-Agent": userAgent } };
|
|
2341
|
-
}
|
|
2342
|
-
function fetchWithUserAgent(userAgent) {
|
|
2343
|
-
return ((input, init) => globalThis.fetch(input, withUserAgent(init, userAgent)));
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
// src/client-info.ts
|
|
2347
|
-
var CLI_PACKAGE = { name: "groundmail-cli", version };
|
|
2348
|
-
|
|
2349
|
-
// src/session/http.ts
|
|
2350
|
-
var CliRequestError = class extends Error {
|
|
2351
|
-
};
|
|
2352
|
-
async function apiRequest(baseUrl, path, options = {}) {
|
|
2353
|
-
const doFetch = fetchWithUserAgent(buildUserAgent(CLI_PACKAGE));
|
|
2354
|
-
const url = `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
2355
|
-
let response;
|
|
2356
2789
|
try {
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
Accept: "application/json",
|
|
2361
|
-
...options.body === void 0 ? {} : { "Content-Type": "application/json" },
|
|
2362
|
-
...options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {}
|
|
2363
|
-
},
|
|
2364
|
-
...options.body === void 0 ? {} : { body: JSON.stringify(options.body) }
|
|
2365
|
-
});
|
|
2790
|
+
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2791
|
+
console.log(JSON.stringify(await client.templates.fromStarter(alias)));
|
|
2792
|
+
return { exitCode: 0 };
|
|
2366
2793
|
} catch (error) {
|
|
2367
|
-
|
|
2368
|
-
throw new CliRequestError(`Could not reach ${baseUrl}: ${reason}`);
|
|
2369
|
-
}
|
|
2370
|
-
const text = await response.text();
|
|
2371
|
-
let body = {};
|
|
2372
|
-
try {
|
|
2373
|
-
const parsed = text ? JSON.parse(text) : {};
|
|
2374
|
-
body = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { value: parsed };
|
|
2375
|
-
} catch {
|
|
2376
|
-
body = { error: text.slice(0, 300) || `HTTP ${response.status}` };
|
|
2794
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2377
2795
|
}
|
|
2378
|
-
return { ok: response.ok, status: response.status, body };
|
|
2379
|
-
}
|
|
2380
|
-
function sentence(text) {
|
|
2381
|
-
return /[.!?…)]$/.test(text.trim()) ? text.trim() : `${text.trim()}.`;
|
|
2382
2796
|
}
|
|
2383
|
-
function
|
|
2384
|
-
const
|
|
2385
|
-
|
|
2386
|
-
if (
|
|
2387
|
-
|
|
2388
|
-
|
|
2797
|
+
async function runListStartersCommand(args, env = process.env) {
|
|
2798
|
+
const options = parseTemplatesCommonArgs(args);
|
|
2799
|
+
const apiKey = resolveApiKey7(options, env);
|
|
2800
|
+
if (!apiKey) {
|
|
2801
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2802
|
+
}
|
|
2803
|
+
try {
|
|
2804
|
+
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2805
|
+
console.log(JSON.stringify(await client.templates.listStarters()));
|
|
2806
|
+
return { exitCode: 0 };
|
|
2807
|
+
} catch (error) {
|
|
2808
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2389
2809
|
}
|
|
2390
|
-
return `${fallback} (HTTP ${response.status}).`;
|
|
2391
2810
|
}
|
|
2392
2811
|
|
|
2393
|
-
// src/
|
|
2394
|
-
var
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2812
|
+
// src/commands/templates/versions.ts
|
|
2813
|
+
var POSITIONAL_FLAGS13 = ["--api-key", "--base-url", "--id", "--alias", "--version", "--from", "--to"];
|
|
2814
|
+
function readFirstPositional14(args) {
|
|
2815
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
2816
|
+
const arg = args[i];
|
|
2817
|
+
if (arg.startsWith("--")) {
|
|
2818
|
+
if (POSITIONAL_FLAGS13.includes(arg)) i += 1;
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
return arg;
|
|
2401
2822
|
}
|
|
2402
|
-
|
|
2403
|
-
function printJson(io, value) {
|
|
2404
|
-
io.out(JSON.stringify(value, null, 2));
|
|
2823
|
+
return void 0;
|
|
2405
2824
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2825
|
+
async function runVersionsCommand(args, env = process.env) {
|
|
2826
|
+
const options = parseTemplatesCommonArgs(args);
|
|
2827
|
+
const apiKey = resolveApiKey7(options, env);
|
|
2828
|
+
if (!apiKey) {
|
|
2829
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2830
|
+
}
|
|
2831
|
+
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional14(args);
|
|
2832
|
+
if (!idOrAlias) {
|
|
2833
|
+
return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
|
|
2834
|
+
}
|
|
2411
2835
|
try {
|
|
2412
|
-
const
|
|
2413
|
-
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2836
|
+
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2837
|
+
console.log(JSON.stringify(await client.templates.listVersions(idOrAlias)));
|
|
2838
|
+
return { exitCode: 0 };
|
|
2839
|
+
} catch (error) {
|
|
2840
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2417
2841
|
}
|
|
2418
2842
|
}
|
|
2419
|
-
function
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
}
|
|
2424
|
-
async function runLoginCommand(argv, deps = {}) {
|
|
2425
|
-
const io = deps.io ?? processIo;
|
|
2426
|
-
const env = deps.env ?? process.env;
|
|
2427
|
-
const sleep = deps.sleep ?? realSleep;
|
|
2428
|
-
const now = deps.now ?? Date.now;
|
|
2429
|
-
const brand = cliBrand(env);
|
|
2430
|
-
const parsed = parseArgs(argv, {
|
|
2431
|
-
values: ["--base-url", "--workspace"],
|
|
2432
|
-
booleans: ["--json", "--browser", "--wait", "--new"]
|
|
2433
|
-
});
|
|
2434
|
-
if (!parsed.ok) return { exitCode: EXIT_USAGE, error: `${parsed.error}
|
|
2435
|
-
Usage: ${brand.bin} login [--workspace <name>] [--new] [--no-browser] [--wait|--no-wait] [--json]` };
|
|
2436
|
-
const { args } = parsed;
|
|
2437
|
-
if (args.positionals.length) {
|
|
2438
|
-
return { exitCode: EXIT_USAGE, error: `${brand.bin} login takes no arguments. To sign in to a workspace by name: ${brand.bin} login --workspace "<name>".` };
|
|
2843
|
+
async function runRestoreCommand(args, env = process.env) {
|
|
2844
|
+
const options = parseTemplatesCommonArgs(args);
|
|
2845
|
+
const apiKey = resolveApiKey7(options, env);
|
|
2846
|
+
if (!apiKey) {
|
|
2847
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2439
2848
|
}
|
|
2440
|
-
const
|
|
2441
|
-
const
|
|
2442
|
-
const
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
);
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
if (current.ok && (trimUrl(current.workspace.apiUrl) || defaultApiUrl(env)) === apiUrl) {
|
|
2454
|
-
const next = `To sign in to another workspace: ${brand.bin} login --workspace "<name>". To sign in as someone else: ${brand.bin} logout, then ${brand.bin} login.`;
|
|
2455
|
-
if (json) {
|
|
2456
|
-
printJson(io, {
|
|
2457
|
-
status: "signed_in",
|
|
2458
|
-
already: true,
|
|
2459
|
-
workspace: { id: current.workspace.id, name: current.workspace.name },
|
|
2460
|
-
email: current.workspace.email ?? null,
|
|
2461
|
-
config_path: configPath(env),
|
|
2462
|
-
next
|
|
2463
|
-
});
|
|
2464
|
-
} else {
|
|
2465
|
-
io.out(`Already signed in${current.workspace.email ? ` as ${current.workspace.email}` : ""} to ${current.workspace.name}.
|
|
2466
|
-
${next}`);
|
|
2467
|
-
}
|
|
2468
|
-
return { exitCode: EXIT_OK };
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
if (!resumed) {
|
|
2472
|
-
const start = await apiRequest(apiUrl, "/cli/login", { body: workspaceHint ? { workspace: workspaceHint } : {}, env });
|
|
2473
|
-
if (!start.ok) return { exitCode: EXIT_FAILED, error: errorMessage(start, "Could not start the sign-in") };
|
|
2474
|
-
const body = start.body;
|
|
2475
|
-
if (typeof body.device_code !== "string" || typeof body.user_code !== "string" || typeof body.verification_url !== "string") {
|
|
2476
|
-
return { exitCode: EXIT_FAILED, error: `Unexpected answer from ${apiUrl}/cli/login. Is --base-url right?` };
|
|
2477
|
-
}
|
|
2478
|
-
login = {
|
|
2479
|
-
deviceCode: body.device_code,
|
|
2480
|
-
userCode: body.user_code,
|
|
2481
|
-
url: body.verification_url,
|
|
2482
|
-
interval: Math.max(1, Number(body.interval) || 2),
|
|
2483
|
-
expiresAt: now() + (Number(body.expires_in) || 600) * 1e3,
|
|
2484
|
-
apiUrl,
|
|
2485
|
-
workspace: workspaceHint
|
|
2486
|
-
};
|
|
2487
|
-
const pending = login;
|
|
2488
|
-
updateConfig((current) => ({ ...current, pendingLogin: pending }), env);
|
|
2489
|
-
}
|
|
2490
|
-
const active = login;
|
|
2491
|
-
const link = `${resumed ? "Still waiting for approval" : `Sign in to ${brand.product}`}: open this link and approve code ${active.userCode}
|
|
2492
|
-
${active.url}`;
|
|
2493
|
-
if (!json) {
|
|
2494
|
-
io.out(`${link}${wait ? "\nWaiting\u2026 (Ctrl-C to stop; run the same command again to pick up where you left off)" : ""}`);
|
|
2495
|
-
} else if (wait) {
|
|
2496
|
-
io.err(link);
|
|
2497
|
-
}
|
|
2498
|
-
if (browser && !resumed && !env.CI) (deps.openBrowser ?? openInBrowser)(active.url);
|
|
2499
|
-
for (; ; ) {
|
|
2500
|
-
const poll = await apiRequest(apiUrl, "/cli/login/token", { body: { device_code: active.deviceCode }, env });
|
|
2501
|
-
if (!poll.ok) {
|
|
2502
|
-
if (poll.status < 500 && poll.status !== 429) {
|
|
2503
|
-
updateConfig(({ pendingLogin: _drop, ...rest }) => rest, env);
|
|
2504
|
-
}
|
|
2505
|
-
return { exitCode: EXIT_FAILED, error: errorMessage(poll, "The sign-in check failed") };
|
|
2506
|
-
}
|
|
2507
|
-
const status = poll.body.status;
|
|
2508
|
-
if (status === "approved" && typeof poll.body.api_key === "string") {
|
|
2509
|
-
return finishLogin(poll.body, active, { io, env, json });
|
|
2510
|
-
}
|
|
2511
|
-
if (status === "denied") {
|
|
2512
|
-
updateConfig(({ pendingLogin: _drop, ...rest }) => rest, env);
|
|
2513
|
-
return { exitCode: EXIT_FAILED, error: `The sign-in was canceled in the browser. Run ${brand.bin} login to start again.` };
|
|
2514
|
-
}
|
|
2515
|
-
if (status !== "pending") {
|
|
2516
|
-
updateConfig(({ pendingLogin: _drop, ...rest }) => rest, env);
|
|
2517
|
-
return { exitCode: EXIT_FAILED, error: `The sign-in code expired or was already used. Run ${brand.bin} login again.` };
|
|
2518
|
-
}
|
|
2519
|
-
if (!wait || now() + active.interval * 1e3 > active.expiresAt) {
|
|
2520
|
-
const next = `Not signed in yet. Approve code ${active.userCode} in the browser, then run ${brand.bin} login again to finish.`;
|
|
2521
|
-
if (json) {
|
|
2522
|
-
printJson(io, {
|
|
2523
|
-
status: "pending",
|
|
2524
|
-
user_code: active.userCode,
|
|
2525
|
-
verification_url: active.url,
|
|
2526
|
-
expires_at: new Date(active.expiresAt).toISOString(),
|
|
2527
|
-
next
|
|
2528
|
-
});
|
|
2529
|
-
} else {
|
|
2530
|
-
io.out(next);
|
|
2531
|
-
}
|
|
2532
|
-
return { exitCode: EXIT_OK };
|
|
2533
|
-
}
|
|
2534
|
-
await sleep(active.interval * 1e3);
|
|
2535
|
-
}
|
|
2849
|
+
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional14(args);
|
|
2850
|
+
const versionRaw = readFlag19(args, "--version");
|
|
2851
|
+
const version2 = versionRaw ? Number.parseInt(versionRaw, 10) : NaN;
|
|
2852
|
+
if (!idOrAlias || !Number.isFinite(version2) || version2 < 1) {
|
|
2853
|
+
return {
|
|
2854
|
+
exitCode: 1,
|
|
2855
|
+
error: "Usage: groundmail templates restore --id <id|alias> --version <n>"
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
try {
|
|
2859
|
+
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2860
|
+
console.log(JSON.stringify(await client.templates.restoreVersion(idOrAlias, version2)));
|
|
2861
|
+
return { exitCode: 0 };
|
|
2536
2862
|
} catch (error) {
|
|
2537
|
-
|
|
2538
|
-
throw error;
|
|
2863
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2539
2864
|
}
|
|
2540
2865
|
}
|
|
2541
|
-
function
|
|
2542
|
-
const
|
|
2543
|
-
const
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
const
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2866
|
+
async function runTestSendCommand(args, env = process.env) {
|
|
2867
|
+
const options = parseTemplatesCommonArgs(args);
|
|
2868
|
+
const apiKey = resolveApiKey7(options, env);
|
|
2869
|
+
if (!apiKey) {
|
|
2870
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2871
|
+
}
|
|
2872
|
+
const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional14(args);
|
|
2873
|
+
const from = readFlag19(args, "--from");
|
|
2874
|
+
const to = readFlag19(args, "--to");
|
|
2875
|
+
if (!idOrAlias || !from || !to) {
|
|
2876
|
+
return {
|
|
2877
|
+
exitCode: 1,
|
|
2878
|
+
error: "Usage: groundmail templates test-send --id <id|alias> --from <email> --to <email>"
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
try {
|
|
2882
|
+
const client = await createTemplatesClient(apiKey, options.baseUrl);
|
|
2883
|
+
console.log(JSON.stringify(await client.templates.testSend(idOrAlias, { from, to })));
|
|
2884
|
+
return { exitCode: 0 };
|
|
2885
|
+
} catch (error) {
|
|
2886
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// src/commands/integrations/shared.ts
|
|
2891
|
+
function readFlag20(args, flag) {
|
|
2892
|
+
const index = args.indexOf(flag);
|
|
2893
|
+
if (index === -1) return void 0;
|
|
2894
|
+
return args[index + 1];
|
|
2895
|
+
}
|
|
2896
|
+
function parseIntegrationsCommonArgs(args) {
|
|
2897
|
+
return {
|
|
2898
|
+
apiKey: readFlag20(args, "--api-key"),
|
|
2899
|
+
baseUrl: readFlag20(args, "--base-url")
|
|
2556
2900
|
};
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
const
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
other_workspaces: others.map((entry) => ({ id: entry.id, name: entry.name })),
|
|
2573
|
-
...envNote ? { note: envNote } : {}
|
|
2574
|
-
});
|
|
2575
|
-
return { exitCode: EXIT_OK };
|
|
2901
|
+
}
|
|
2902
|
+
function resolveApiKey8(options, env) {
|
|
2903
|
+
return options.apiKey || env.GROUNDMAIL_API_KEY || null;
|
|
2904
|
+
}
|
|
2905
|
+
async function createIntegrationsClient(apiKey, baseUrl) {
|
|
2906
|
+
const { Groundmail } = await import("groundmail");
|
|
2907
|
+
return new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// src/commands/integrations/twilio.ts
|
|
2911
|
+
async function runTwilioStatusCommand(args, env = process.env) {
|
|
2912
|
+
const options = parseIntegrationsCommonArgs(args);
|
|
2913
|
+
const apiKey = resolveApiKey8(options, env);
|
|
2914
|
+
if (!apiKey) {
|
|
2915
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2576
2916
|
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
);
|
|
2917
|
+
try {
|
|
2918
|
+
const client = await createIntegrationsClient(apiKey, options.baseUrl);
|
|
2919
|
+
const result = await client.integrations.getTwilio();
|
|
2920
|
+
console.log(JSON.stringify(result));
|
|
2921
|
+
return { exitCode: 0 };
|
|
2922
|
+
} catch (error) {
|
|
2923
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2584
2924
|
}
|
|
2585
|
-
if (envNote) lines.push(envNote);
|
|
2586
|
-
lines.push(`Next: ${brand.bin} domains list`);
|
|
2587
|
-
io.out(lines.join("\n"));
|
|
2588
|
-
return { exitCode: EXIT_OK };
|
|
2589
2925
|
}
|
|
2590
|
-
async function
|
|
2591
|
-
const
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
if (!parsed.ok) return { exitCode: EXIT_USAGE, error: `${parsed.error}
|
|
2596
|
-
Usage: ${brand.bin} logout [--workspace <name>] [--all]` };
|
|
2597
|
-
const { args } = parsed;
|
|
2598
|
-
const json = wantsJson(args, io);
|
|
2599
|
-
const config = loadConfig(env);
|
|
2600
|
-
const all = savedWorkspaces(config);
|
|
2601
|
-
let targets;
|
|
2602
|
-
if (args.booleans["--all"]) {
|
|
2603
|
-
targets = all;
|
|
2604
|
-
} else if (args.values["--workspace"]) {
|
|
2605
|
-
const match = findSavedWorkspace(config, args.values["--workspace"]);
|
|
2606
|
-
if (!match.ok) {
|
|
2607
|
-
return { exitCode: EXIT_FAILED, error: `Not signed in to "${args.values["--workspace"]}" on this computer.` };
|
|
2608
|
-
}
|
|
2609
|
-
targets = [match.workspace];
|
|
2610
|
-
} else {
|
|
2611
|
-
const current = config.current ? config.workspaces?.[config.current] : all.length === 1 ? all[0] : void 0;
|
|
2612
|
-
if (!current && all.length > 1) {
|
|
2613
|
-
return {
|
|
2614
|
-
exitCode: EXIT_USAGE,
|
|
2615
|
-
error: `Signed in to ${all.length} workspaces and none is picked. Say which: ${brand.bin} logout --workspace <name>, or ${brand.bin} logout --all.`
|
|
2616
|
-
};
|
|
2617
|
-
}
|
|
2618
|
-
targets = current ? [current] : [];
|
|
2926
|
+
async function runTwilioConnectCommand(args, env = process.env) {
|
|
2927
|
+
const options = parseIntegrationsCommonArgs(args);
|
|
2928
|
+
const apiKey = resolveApiKey8(options, env);
|
|
2929
|
+
if (!apiKey) {
|
|
2930
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2619
2931
|
}
|
|
2620
|
-
const
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
} catch (error) {
|
|
2630
|
-
warning = error instanceof Error ? error.message : String(error);
|
|
2631
|
-
}
|
|
2632
|
-
results.push({ id: workspace.id, name: workspace.name, revoked, ...warning ? { warning } : {} });
|
|
2932
|
+
const accountSid = readFlag20(args, "--account-sid") || env.TWILIO_ACCOUNT_SID || env.TWILIO_SID || "";
|
|
2933
|
+
const authToken = readFlag20(args, "--auth-token") || env.TWILIO_AUTH_TOKEN || env.TWILIO_TOKEN || "";
|
|
2934
|
+
const defaultFrom = readFlag20(args, "--from") || readFlag20(args, "--default-from") || env.TWILIO_DEFAULT_FROM || "";
|
|
2935
|
+
const messagingServiceSid = readFlag20(args, "--messaging-service-sid") || readFlag20(args, "--messaging-sid") || env.TWILIO_MESSAGING_SERVICE_SID || "";
|
|
2936
|
+
if (!accountSid.trim() || !authToken.trim()) {
|
|
2937
|
+
return {
|
|
2938
|
+
exitCode: 1,
|
|
2939
|
+
error: "Required: --account-sid and --auth-token (or TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN)."
|
|
2940
|
+
};
|
|
2633
2941
|
}
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
Object.entries(current.workspaces ?? {}).filter(([id]) => !removing.has(id))
|
|
2640
|
-
);
|
|
2641
|
-
const { pendingLogin: _drop, current: picked, ...rest } = current;
|
|
2642
|
-
return { ...rest, workspaces, ...picked && !removing.has(picked) ? { current: picked } : {} };
|
|
2643
|
-
}, env);
|
|
2942
|
+
if (!defaultFrom.trim() && !messagingServiceSid.trim()) {
|
|
2943
|
+
return {
|
|
2944
|
+
exitCode: 1,
|
|
2945
|
+
error: "Required: --from or --messaging-service-sid (or TWILIO_DEFAULT_FROM / TWILIO_MESSAGING_SERVICE_SID)."
|
|
2946
|
+
};
|
|
2644
2947
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2948
|
+
try {
|
|
2949
|
+
const client = await createIntegrationsClient(apiKey, options.baseUrl);
|
|
2950
|
+
const result = await client.integrations.connectTwilio({
|
|
2951
|
+
accountSid: accountSid.trim(),
|
|
2952
|
+
authToken: authToken.trim(),
|
|
2953
|
+
...defaultFrom.trim() ? { defaultFrom: defaultFrom.trim() } : {},
|
|
2954
|
+
...messagingServiceSid.trim() ? { messagingServiceSid: messagingServiceSid.trim() } : {}
|
|
2955
|
+
});
|
|
2956
|
+
console.log(JSON.stringify(result));
|
|
2957
|
+
return { exitCode: 0 };
|
|
2958
|
+
} catch (error) {
|
|
2959
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2649
2960
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2961
|
+
}
|
|
2962
|
+
async function runTwilioDisconnectCommand(args, env = process.env) {
|
|
2963
|
+
const options = parseIntegrationsCommonArgs(args);
|
|
2964
|
+
const apiKey = resolveApiKey8(options, env);
|
|
2965
|
+
if (!apiKey) {
|
|
2966
|
+
return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
|
|
2967
|
+
}
|
|
2968
|
+
try {
|
|
2969
|
+
const client = await createIntegrationsClient(apiKey, options.baseUrl);
|
|
2970
|
+
const result = await client.integrations.disconnectTwilio();
|
|
2971
|
+
console.log(JSON.stringify(result));
|
|
2972
|
+
return { exitCode: 0 };
|
|
2973
|
+
} catch (error) {
|
|
2974
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2653
2975
|
}
|
|
2654
|
-
const lines = results.map(
|
|
2655
|
-
(result) => `Signed out of ${result.name}: ${result.revoked ? "the key was revoked and" : "the key was"} removed from ${configPath(env)}.${result.warning ? ` (Could not revoke it: ${result.warning} Revoke it on the API keys page.)` : ""}`
|
|
2656
|
-
);
|
|
2657
|
-
if (envNote) lines.push(envNote);
|
|
2658
|
-
io.out(lines.join("\n"));
|
|
2659
|
-
return { exitCode: EXIT_OK };
|
|
2660
2976
|
}
|
|
2661
|
-
|
|
2977
|
+
|
|
2978
|
+
// src/commands/auth.ts
|
|
2979
|
+
async function runAuthCommand(subcommand, argv, deps = {}) {
|
|
2980
|
+
const env = deps.env ?? process.env;
|
|
2981
|
+
const brand = cliBrand(env);
|
|
2982
|
+
const usage = `Usage: ${brand.bin} auth token [--scope <scopes>] [--workspace <name>] [--json]`;
|
|
2983
|
+
if (subcommand === "token") return printToken(argv, deps, usage);
|
|
2984
|
+
return { exitCode: EXIT_USAGE, error: subcommand ? `Unknown: auth ${subcommand}.
|
|
2985
|
+
${usage}` : usage };
|
|
2986
|
+
}
|
|
2987
|
+
async function printToken(argv, deps, usage) {
|
|
2662
2988
|
const io = deps.io ?? processIo;
|
|
2663
2989
|
const env = deps.env ?? process.env;
|
|
2664
2990
|
const brand = cliBrand(env);
|
|
2665
|
-
const parsed = parseArgs(argv, { values: [
|
|
2991
|
+
const parsed = parseArgs(argv, { values: ["--scope", "--workspace"], booleans: ["--json"] });
|
|
2666
2992
|
if (!parsed.ok) return { exitCode: EXIT_USAGE, error: `${parsed.error}
|
|
2667
|
-
|
|
2993
|
+
${usage}` };
|
|
2668
2994
|
const { args } = parsed;
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
const
|
|
2995
|
+
if (args.positionals.length) return { exitCode: EXIT_USAGE, error: usage };
|
|
2996
|
+
const scopeFlag = args.values["--scope"] ? parseScopeFlag(args.values["--scope"]) : null;
|
|
2997
|
+
if (scopeFlag && !scopeFlag.ok) return { exitCode: EXIT_USAGE, error: `${scopeFlag.error}
|
|
2998
|
+
${usage}` };
|
|
2999
|
+
const config = loadConfig(env);
|
|
3000
|
+
const picked = args.values["--workspace"] ? findSavedWorkspace(config, args.values["--workspace"]) : currentSavedWorkspace(config, env);
|
|
3001
|
+
if (!picked.ok) {
|
|
3002
|
+
const reason = "error" in picked ? picked.error : `Not signed in to "${args.values["--workspace"]}" on this computer. Run ${brand.bin} login --workspace "${args.values["--workspace"]}".`;
|
|
3003
|
+
return { exitCode: EXIT_FAILED, error: reason };
|
|
3004
|
+
}
|
|
3005
|
+
const workspace = picked.workspace;
|
|
3006
|
+
if (!workspace.oauth) {
|
|
3007
|
+
const envNote = envApiKey(env) ? ` ${brand.keyEnvVar} is an API key already; use it as the Bearer token directly.` : "";
|
|
3008
|
+
return {
|
|
3009
|
+
exitCode: EXIT_FAILED,
|
|
3010
|
+
error: `The saved sign-in for ${workspace.name} is an API key from an older ${brand.bin} login, not OAuth. Run ${brand.bin} login to switch to tokens.${envNote}`
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
2675
3013
|
try {
|
|
2676
|
-
const
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
if (
|
|
2684
|
-
printJson(io, {
|
|
2685
|
-
|
|
3014
|
+
const apiUrl = trimUrl(workspace.apiUrl) || defaultApiUrl(env);
|
|
3015
|
+
const result = await accessTokenFor(workspace, apiUrl, {
|
|
3016
|
+
scope: scopeFlag?.ok ? scopeFlag.scope : null,
|
|
3017
|
+
env,
|
|
3018
|
+
now: deps.now
|
|
3019
|
+
});
|
|
3020
|
+
if (!result.ok) return { exitCode: EXIT_FAILED, error: result.error };
|
|
3021
|
+
if (args.booleans["--json"]) {
|
|
3022
|
+
printJson(io, { access_token: result.token, token_type: "Bearer", scope: result.scope, workspace: { id: workspace.id, name: workspace.name } });
|
|
3023
|
+
} else {
|
|
3024
|
+
io.out(result.token);
|
|
2686
3025
|
}
|
|
2687
|
-
io.out(
|
|
2688
|
-
[
|
|
2689
|
-
session.user?.email ?? "(an API key; no person is recorded for it)",
|
|
2690
|
-
`Workspace: ${session.workspace?.name ?? "?"} (${session.workspace?.id ?? "?"})`,
|
|
2691
|
-
`Key: ${session.key?.name ?? "?"} (${session.key?.prefix ?? "?"}\u2026${session.key?.scope === "sending" ? ", sending only" : ""}), from ${source}`,
|
|
2692
|
-
...auth.baseUrl !== defaultApiUrl(env) ? [`API: ${auth.baseUrl}`] : []
|
|
2693
|
-
].join("\n")
|
|
2694
|
-
);
|
|
2695
3026
|
return { exitCode: EXIT_OK };
|
|
2696
3027
|
} catch (error) {
|
|
2697
3028
|
if (error instanceof CliRequestError) return { exitCode: EXIT_FAILED, error: error.message };
|
|
@@ -2780,78 +3111,132 @@ async function switchWorkspace(argv, deps) {
|
|
|
2780
3111
|
return { exitCode: EXIT_OK };
|
|
2781
3112
|
}
|
|
2782
3113
|
|
|
3114
|
+
// src/help.ts
|
|
3115
|
+
var USAGE_LINES = [
|
|
3116
|
+
" groundmail login [--scope <scopes>] [--workspace <name>] sign in through the browser (OAuth; no key to copy)",
|
|
3117
|
+
" groundmail logout [--workspace <name>|--all] revokes the sign-in server-side",
|
|
3118
|
+
" groundmail whoami",
|
|
3119
|
+
" groundmail auth token [--scope <scopes>] print a short-lived access token (for agents and scripts)",
|
|
3120
|
+
" groundmail workspaces list",
|
|
3121
|
+
" groundmail workspaces use <name|id>",
|
|
3122
|
+
" groundmail domains add example.com",
|
|
3123
|
+
" groundmail domains apply example.com --provider cloudflare",
|
|
3124
|
+
" groundmail domains apply example.com --provider vercel",
|
|
3125
|
+
" groundmail domains apply example.com --provider godaddy",
|
|
3126
|
+
" groundmail domains list",
|
|
3127
|
+
" groundmail domains get example.com",
|
|
3128
|
+
" groundmail domains update example.com --inbound true",
|
|
3129
|
+
" groundmail domains verify example.com",
|
|
3130
|
+
" groundmail domains delete example.com",
|
|
3131
|
+
" groundmail received-emails list [--domain inbound.example.com]",
|
|
3132
|
+
" groundmail received-emails get inb_...",
|
|
3133
|
+
' groundmail emails send --from you@domain.com --to user@example.com --subject "Hello" --html "<p>Hi</p>" [--category transactional|product|newsletter] [--unsubscribe]',
|
|
3134
|
+
" groundmail emails batch --file ./emails.json",
|
|
3135
|
+
" groundmail emails list [--limit 25] [--cursor \u2026]",
|
|
3136
|
+
" groundmail emails insights [--window 7d|30d]",
|
|
3137
|
+
" groundmail emails get --id msg_\u2026",
|
|
3138
|
+
" groundmail emails cancel --id msg_\u2026",
|
|
3139
|
+
" groundmail emails resend --id msg_\u2026",
|
|
3140
|
+
` groundmail events send --name user.created --email user@example.com --data '{"name":"Ada"}'`,
|
|
3141
|
+
" groundmail automations list",
|
|
3142
|
+
" groundmail automations create --file ./welcome.json",
|
|
3143
|
+
` groundmail automations create --name Welcome --trigger user.created --steps '[{"type":"send_email","email":{...}}]'`,
|
|
3144
|
+
" groundmail automations create --file ./otp-sms.json",
|
|
3145
|
+
" groundmail automations get --id <automation-id>",
|
|
3146
|
+
" groundmail automations update --id <automation-id> --file ./welcome.json",
|
|
3147
|
+
" groundmail automations activate --id <automation-id>",
|
|
3148
|
+
" groundmail automations pause --id <automation-id>",
|
|
3149
|
+
" groundmail automations delete --id <automation-id>",
|
|
3150
|
+
" groundmail webhooks list",
|
|
3151
|
+
" groundmail webhooks create --url https://yourapp.com/hooks",
|
|
3152
|
+
" groundmail webhooks get --id wh_\u2026",
|
|
3153
|
+
" groundmail webhooks update --id wh_\u2026 [--url \u2026] [--event email.sent] [--enabled true|false]",
|
|
3154
|
+
" groundmail webhooks delete --id <webhook-id>",
|
|
3155
|
+
" groundmail suppressions list",
|
|
3156
|
+
" groundmail suppressions add --email user@example.com [--reason \u2026]",
|
|
3157
|
+
" groundmail suppressions remove --id <id> | --email user@example.com",
|
|
3158
|
+
" groundmail templates list [--status draft|published]",
|
|
3159
|
+
" groundmail templates starters",
|
|
3160
|
+
" groundmail templates from-starter password-reset",
|
|
3161
|
+
' groundmail templates create --name Welcome --subject "Hi" --html "<p>Hi</p>" [--category transactional|product|newsletter] [--unsubscribe-enabled]',
|
|
3162
|
+
" groundmail templates get --id tpl_\u2026",
|
|
3163
|
+
" groundmail templates update --id tpl_\u2026 [--name \u2026] [--subject \u2026] [--html \u2026] [--text \u2026] [--set-alias \u2026] [--category transactional|product|newsletter] [--unsubscribe-enabled|--no-unsubscribe-enabled]",
|
|
3164
|
+
" groundmail templates publish --id tpl_\u2026",
|
|
3165
|
+
` groundmail templates push ./emails/welcome.tsx --alias welcome [--subject "Welcome"] [--props '{"name":"Ada"}'] [--publish]`,
|
|
3166
|
+
" groundmail templates versions --id tpl_\u2026",
|
|
3167
|
+
" groundmail templates restore --id tpl_\u2026 --version 2",
|
|
3168
|
+
" groundmail templates test-send --id tpl_\u2026 --from you@domain.com --to you@domain.com",
|
|
3169
|
+
' groundmail templates duplicate --id tpl_\u2026 [--name "Copy"]',
|
|
3170
|
+
" groundmail templates delete --id tpl_\u2026",
|
|
3171
|
+
" groundmail keys list",
|
|
3172
|
+
" groundmail keys create [--name Default] [--scopes emails:send,domains:read] [--allowed-domains example.com]",
|
|
3173
|
+
" groundmail keys delete --id <api-key-id>",
|
|
3174
|
+
" groundmail integrations twilio status",
|
|
3175
|
+
" groundmail integrations twilio connect --account-sid AC\u2026 --auth-token \u2026 --from +15551234567",
|
|
3176
|
+
" groundmail integrations twilio disconnect"
|
|
3177
|
+
];
|
|
3178
|
+
var FOOTER_LINES = [
|
|
3179
|
+
"Environment:",
|
|
3180
|
+
" GROUNDMAIL_API_KEY=gm_\u2026 optional (CI): wins over the `groundmail login` sign-in",
|
|
3181
|
+
" GROUNDMAIL_NO_KEYCHAIN=1 keep the refresh token in the config file (0600) instead of the OS keychain",
|
|
3182
|
+
" TWILIO_ACCOUNT_SID=AC\u2026 TWILIO_AUTH_TOKEN=\u2026 TWILIO_DEFAULT_FROM=+1\u2026 (integrations twilio connect)",
|
|
3183
|
+
" CLOUDFLARE_API_TOKEN=\u2026 (optional local apply; else uses dashboard Integrations)",
|
|
3184
|
+
" VERCEL_API_TOKEN=\u2026 VERCEL_TEAM_ID=\u2026 (optional local apply; else uses dashboard Integrations)",
|
|
3185
|
+
" GODADDY_API_KEY=\u2026 GODADDY_API_SECRET=\u2026 (domains apply --provider godaddy)",
|
|
3186
|
+
"",
|
|
3187
|
+
"Scopes: full, emails:send, emails:read, domains:read, domains:write, webhooks:write, templates:write,",
|
|
3188
|
+
" suppressions:write, account:read",
|
|
3189
|
+
"",
|
|
3190
|
+
"Add --help to any command for just its usage. --version prints the CLI version."
|
|
3191
|
+
];
|
|
3192
|
+
var CLI_VERSION = version;
|
|
3193
|
+
var ALIASES = {
|
|
3194
|
+
workspace: { "": "workspaces" },
|
|
3195
|
+
domains: { create: "add", remove: "delete" },
|
|
3196
|
+
events: { trigger: "send" },
|
|
3197
|
+
automations: { add: "create", remove: "delete" },
|
|
3198
|
+
suppressions: { create: "add", delete: "remove" },
|
|
3199
|
+
templates: { add: "create", remove: "delete" }
|
|
3200
|
+
};
|
|
3201
|
+
function words(line) {
|
|
3202
|
+
return line.trim().split(/\s+/);
|
|
3203
|
+
}
|
|
3204
|
+
function fullUsage() {
|
|
3205
|
+
return ["Usage:", ...USAGE_LINES, "", ...FOOTER_LINES].join("\n");
|
|
3206
|
+
}
|
|
3207
|
+
function usageFor(command, subcommand) {
|
|
3208
|
+
if (!command) return fullUsage();
|
|
3209
|
+
const name = ALIASES[command]?.[""] ?? command;
|
|
3210
|
+
const forCommand = USAGE_LINES.filter((line) => words(line)[1] === name);
|
|
3211
|
+
if (!forCommand.length) return null;
|
|
3212
|
+
const sub = subcommand && !subcommand.startsWith("-") ? ALIASES[name]?.[subcommand] ?? subcommand : void 0;
|
|
3213
|
+
const forSub = sub ? forCommand.filter((line) => words(line)[2] === sub) : [];
|
|
3214
|
+
return ["Usage:", ...forSub.length ? forSub : forCommand].join("\n");
|
|
3215
|
+
}
|
|
3216
|
+
function isHelpFlag(arg) {
|
|
3217
|
+
return arg === "--help" || arg === "-h";
|
|
3218
|
+
}
|
|
3219
|
+
function isVersionFlag(arg) {
|
|
3220
|
+
return arg === "--version" || arg === "-v" || arg === "-V";
|
|
3221
|
+
}
|
|
3222
|
+
function resolveMetaRequest(argv) {
|
|
3223
|
+
const [command, subcommand] = argv;
|
|
3224
|
+
if (isVersionFlag(command) || command === "version") return { kind: "version", text: CLI_VERSION };
|
|
3225
|
+
if (!command) return null;
|
|
3226
|
+
if (isHelpFlag(command)) return { kind: "help", text: fullUsage() };
|
|
3227
|
+
if (command === "help") {
|
|
3228
|
+
return { kind: "help", text: usageFor(argv[1], argv[2]) ?? fullUsage() };
|
|
3229
|
+
}
|
|
3230
|
+
if (argv.some(isHelpFlag)) {
|
|
3231
|
+
const sub = isHelpFlag(subcommand) ? void 0 : subcommand;
|
|
3232
|
+
return { kind: "help", text: usageFor(command, sub) ?? fullUsage() };
|
|
3233
|
+
}
|
|
3234
|
+
return null;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
2783
3237
|
// src/cli.ts
|
|
2784
3238
|
function printUsage() {
|
|
2785
|
-
console.error(
|
|
2786
|
-
console.error(" groundmail login [--workspace <name>] sign in through the browser (no key to copy)");
|
|
2787
|
-
console.error(" groundmail logout [--workspace <name>|--all]");
|
|
2788
|
-
console.error(" groundmail whoami");
|
|
2789
|
-
console.error(" groundmail workspaces list");
|
|
2790
|
-
console.error(" groundmail workspaces use <name|id>");
|
|
2791
|
-
console.error(" groundmail domains add example.com");
|
|
2792
|
-
console.error(" groundmail domains apply example.com --provider cloudflare");
|
|
2793
|
-
console.error(" groundmail domains apply example.com --provider vercel");
|
|
2794
|
-
console.error(" groundmail domains apply example.com --provider godaddy");
|
|
2795
|
-
console.error(" groundmail domains list");
|
|
2796
|
-
console.error(" groundmail domains get example.com");
|
|
2797
|
-
console.error(" groundmail domains update example.com --inbound true");
|
|
2798
|
-
console.error(" groundmail domains verify example.com");
|
|
2799
|
-
console.error(" groundmail domains delete example.com");
|
|
2800
|
-
console.error(" groundmail received-emails list [--domain inbound.example.com]");
|
|
2801
|
-
console.error(" groundmail received-emails get inb_...");
|
|
2802
|
-
console.error(' groundmail emails send --from you@domain.com --to user@example.com --subject "Hello" --html "<p>Hi</p>" [--category transactional|product|newsletter] [--unsubscribe]');
|
|
2803
|
-
console.error(" groundmail emails batch --file ./emails.json");
|
|
2804
|
-
console.error(" groundmail emails list [--limit 25] [--cursor \u2026]");
|
|
2805
|
-
console.error(" groundmail emails insights [--window 7d|30d]");
|
|
2806
|
-
console.error(" groundmail emails get --id msg_\u2026");
|
|
2807
|
-
console.error(" groundmail emails cancel --id msg_\u2026");
|
|
2808
|
-
console.error(" groundmail emails resend --id msg_\u2026");
|
|
2809
|
-
console.error(` groundmail events send --name user.created --email user@example.com --data '{"name":"Ada"}'`);
|
|
2810
|
-
console.error(" groundmail automations list");
|
|
2811
|
-
console.error(" groundmail automations create --file ./welcome.json");
|
|
2812
|
-
console.error(` groundmail automations create --name Welcome --trigger user.created --steps '[{"type":"send_email","email":{...}}]'`);
|
|
2813
|
-
console.error(" groundmail automations create --file ./otp-sms.json");
|
|
2814
|
-
console.error(" groundmail automations get --id <automation-id>");
|
|
2815
|
-
console.error(" groundmail automations update --id <automation-id> --file ./welcome.json");
|
|
2816
|
-
console.error(" groundmail automations activate --id <automation-id>");
|
|
2817
|
-
console.error(" groundmail automations pause --id <automation-id>");
|
|
2818
|
-
console.error(" groundmail automations delete --id <automation-id>");
|
|
2819
|
-
console.error(" groundmail webhooks list");
|
|
2820
|
-
console.error(" groundmail webhooks create --url https://yourapp.com/hooks");
|
|
2821
|
-
console.error(" groundmail webhooks get --id wh_\u2026");
|
|
2822
|
-
console.error(" groundmail webhooks update --id wh_\u2026 [--url \u2026] [--event email.sent] [--enabled true|false]");
|
|
2823
|
-
console.error(" groundmail webhooks delete --id <webhook-id>");
|
|
2824
|
-
console.error(" groundmail suppressions list");
|
|
2825
|
-
console.error(" groundmail suppressions add --email user@example.com [--reason \u2026]");
|
|
2826
|
-
console.error(" groundmail suppressions remove --id <id> | --email user@example.com");
|
|
2827
|
-
console.error(" groundmail templates list [--status draft|published]");
|
|
2828
|
-
console.error(" groundmail templates starters");
|
|
2829
|
-
console.error(" groundmail templates from-starter password-reset");
|
|
2830
|
-
console.error(' groundmail templates create --name Welcome --subject "Hi" --html "<p>Hi</p>" [--category transactional|product|newsletter] [--unsubscribe-enabled]');
|
|
2831
|
-
console.error(" groundmail templates get --id tpl_\u2026");
|
|
2832
|
-
console.error(" groundmail templates update --id tpl_\u2026 [--name \u2026] [--subject \u2026] [--html \u2026] [--text \u2026] [--set-alias \u2026] [--category transactional|product|newsletter] [--unsubscribe-enabled|--no-unsubscribe-enabled]");
|
|
2833
|
-
console.error(" groundmail templates publish --id tpl_\u2026");
|
|
2834
|
-
console.error(
|
|
2835
|
-
` groundmail templates push ./emails/welcome.tsx --alias welcome [--subject "Welcome"] [--props '{"name":"Ada"}'] [--publish]`
|
|
2836
|
-
);
|
|
2837
|
-
console.error(" groundmail templates versions --id tpl_\u2026");
|
|
2838
|
-
console.error(" groundmail templates restore --id tpl_\u2026 --version 2");
|
|
2839
|
-
console.error(" groundmail templates test-send --id tpl_\u2026 --from you@domain.com --to you@domain.com");
|
|
2840
|
-
console.error(' groundmail templates duplicate --id tpl_\u2026 [--name "Copy"]');
|
|
2841
|
-
console.error(" groundmail templates delete --id tpl_\u2026");
|
|
2842
|
-
console.error(" groundmail keys list");
|
|
2843
|
-
console.error(" groundmail keys create [--name Default] [--scope full|sending]");
|
|
2844
|
-
console.error(" groundmail keys delete --id <api-key-id>");
|
|
2845
|
-
console.error(" groundmail integrations twilio status");
|
|
2846
|
-
console.error(" groundmail integrations twilio connect --account-sid AC\u2026 --auth-token \u2026 --from +15551234567");
|
|
2847
|
-
console.error(" groundmail integrations twilio disconnect");
|
|
2848
|
-
console.error("");
|
|
2849
|
-
console.error("Environment:");
|
|
2850
|
-
console.error(" GROUNDMAIL_API_KEY=gm_\u2026 optional: `groundmail login` saves a key instead");
|
|
2851
|
-
console.error(" TWILIO_ACCOUNT_SID=AC\u2026 TWILIO_AUTH_TOKEN=\u2026 TWILIO_DEFAULT_FROM=+1\u2026 (integrations twilio connect)");
|
|
2852
|
-
console.error(" CLOUDFLARE_API_TOKEN=\u2026 (optional local apply; else uses dashboard Integrations)");
|
|
2853
|
-
console.error(" VERCEL_API_TOKEN=\u2026 VERCEL_TEAM_ID=\u2026 (optional local apply; else uses dashboard Integrations)");
|
|
2854
|
-
console.error(" GODADDY_API_KEY=\u2026 GODADDY_API_SECRET=\u2026 (domains apply --provider godaddy)");
|
|
3239
|
+
console.error(fullUsage());
|
|
2855
3240
|
}
|
|
2856
3241
|
async function finish(result) {
|
|
2857
3242
|
if (result.error) console.error(result.error);
|
|
@@ -2860,15 +3245,17 @@ async function finish(result) {
|
|
|
2860
3245
|
async function main() {
|
|
2861
3246
|
const [, , command, subcommand] = process.argv;
|
|
2862
3247
|
const args = process.argv.slice(3);
|
|
2863
|
-
|
|
2864
|
-
|
|
3248
|
+
const meta = resolveMetaRequest(process.argv.slice(2));
|
|
3249
|
+
if (meta) {
|
|
3250
|
+
console.log(meta.text);
|
|
2865
3251
|
process.exit(0);
|
|
2866
3252
|
}
|
|
2867
3253
|
if (command === "login") await finish(await runLoginCommand(args));
|
|
2868
3254
|
if (command === "logout") await finish(await runLogoutCommand(args));
|
|
2869
3255
|
if (command === "whoami") await finish(await runWhoamiCommand(args));
|
|
2870
3256
|
if (command === "workspaces" || command === "workspace") await finish(await runWorkspacesCommand(subcommand, args.slice(1)));
|
|
2871
|
-
|
|
3257
|
+
if (command === "auth") await finish(await runAuthCommand(subcommand, args.slice(1)));
|
|
3258
|
+
const session = await applySavedSession(process.argv.slice(4));
|
|
2872
3259
|
if (session.notice) console.error(session.notice);
|
|
2873
3260
|
const rest = session.argv;
|
|
2874
3261
|
if (command === "domains") {
|