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.
Files changed (3) hide show
  1. package/README.md +19 -5
  2. package/dist/cli.js +1695 -1308
  3. 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/commands/keys/create.ts
1268
- function readFlag16(args, flag) {
1269
- const index = args.indexOf(flag);
1270
- if (index === -1) return void 0;
1271
- return args[index + 1];
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
- async function runCreateKeyCommandWithClient(args, env = process.env) {
1274
- const apiKey = readFlag16(args, "--api-key") || env.GROUNDMAIL_API_KEY;
1275
- const baseUrl = readFlag16(args, "--base-url");
1276
- if (!apiKey) {
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
- // src/commands/keys/delete.ts
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
- async function runDeleteKeyCommandWithClient(args, env = process.env) {
1307
- const apiKey = readFlag17(args, "--api-key") || env.GROUNDMAIL_API_KEY;
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
- // src/commands/suppressions/shared.ts
1330
- function readFlag18(args, flag) {
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/commands/suppressions/list.ts
1350
- async function runListCommand6(args, env = process.env) {
1351
- const options = parseSuppressionsCommonArgs(args);
1352
- const apiKey = resolveApiKey6(options, env);
1353
- if (!apiKey) {
1354
- return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
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
- try {
1357
- const client = await createSuppressionsClient(apiKey, options.baseUrl);
1358
- const limitRaw = readFlag18(args, "--limit");
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/commands/suppressions/add.ts
1374
- async function runAddCommand2(args, env = process.env) {
1375
- const options = parseSuppressionsCommonArgs(args);
1376
- const apiKey = resolveApiKey6(options, env);
1377
- if (!apiKey) {
1378
- return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
1379
- }
1380
- const email = readFlag18(args, "--email") || args.find((a) => a.includes("@") && !a.startsWith("-"));
1381
- if (!email) {
1382
- return { exitCode: 1, error: "Missing email. Pass --email user@example.com" };
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
- const client = await createSuppressionsClient(apiKey, options.baseUrl);
1386
- const reason = readFlag18(args, "--reason");
1387
- const result = await client.suppressions.create({
1388
- email,
1389
- ...reason ? { reason } : {}
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
- return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
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 client = await createSuppressionsClient(apiKey, options.baseUrl);
1411
- const result = await client.suppressions.remove(idOrEmail);
1412
- console.log(JSON.stringify(result));
1413
- return { exitCode: 0 };
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
- // src/commands/templates/shared.ts
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 readFirstPositional8(args) {
1456
- for (let i = 0; i < args.length; i += 1) {
1457
- const arg = args[i];
1458
- if (arg.startsWith("--")) {
1459
- if (POSITIONAL_FLAGS7.includes(arg)) i += 1;
1460
- continue;
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 void 0;
1360
+ return `${fallback} (HTTP ${response.status}).`;
1465
1361
  }
1466
1362
 
1467
- // src/commands/templates/duplicate.ts
1468
- var POSITIONAL_FLAGS8 = ["--api-key", "--base-url", "--id", "--alias", "--name"];
1469
- async function runDuplicateCommand(args, env = process.env) {
1470
- const options = parseTemplatesCommonArgs(args);
1471
- const apiKey = resolveApiKey7(options, env);
1472
- if (!apiKey) return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
1473
- const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional9(args);
1474
- if (!idOrAlias) return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
1475
- const name = readFlag19(args, "--name");
1476
- try {
1477
- const client = await createTemplatesClient(apiKey, options.baseUrl);
1478
- console.log(JSON.stringify(await client.templates.duplicate(idOrAlias, name ? { name } : {})));
1479
- return { exitCode: 0 };
1480
- } catch (error) {
1481
- return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
1482
- }
1483
- }
1484
- function readFirstPositional9(args) {
1485
- for (let i = 0; i < args.length; i += 1) {
1486
- const arg = args[i];
1487
- if (arg.startsWith("--")) {
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
- return arg;
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 void 0;
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/commands/templates/get.ts
1497
- var POSITIONAL_FLAGS9 = ["--api-key", "--base-url", "--id", "--alias"];
1498
- async function runGetCommand6(args, env = process.env) {
1499
- const options = parseTemplatesCommonArgs(args);
1500
- const apiKey = resolveApiKey7(options, env);
1501
- if (!apiKey) return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
1502
- const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional10(args);
1503
- if (!idOrAlias) return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
1504
- try {
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 readFirstPositional10(args) {
1513
- for (let i = 0; i < args.length; i += 1) {
1514
- const arg = args[i];
1515
- if (arg.startsWith("--")) {
1516
- if (POSITIONAL_FLAGS9.includes(arg)) i += 1;
1517
- continue;
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 arg;
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
- // src/commands/templates/list.ts
1525
- async function runListCommand7(args, env = process.env) {
1526
- const options = parseTemplatesCommonArgs(args);
1527
- const apiKey = resolveApiKey7(options, env);
1528
- if (!apiKey) {
1529
- return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
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
- const client = await createTemplatesClient(apiKey, options.baseUrl);
1533
- const limitRaw = readFlag19(args, "--limit");
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/commands/templates/publish.ts
1549
- var POSITIONAL_FLAGS10 = ["--api-key", "--base-url", "--id", "--alias"];
1550
- async function runPublishCommand(args, env = process.env) {
1551
- const options = parseTemplatesCommonArgs(args);
1552
- const apiKey = resolveApiKey7(options, env);
1553
- if (!apiKey) return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
1554
- const idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional11(args);
1555
- if (!idOrAlias) return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
1556
- try {
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 readFirstPositional11(args) {
1565
- for (let i = 0; i < args.length; i += 1) {
1566
- const arg = args[i];
1567
- if (arg.startsWith("--")) {
1568
- if (POSITIONAL_FLAGS10.includes(arg)) i += 1;
1569
- continue;
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
- return arg;
1572
- }
1573
- return void 0;
1535
+ };
1536
+ return cached;
1574
1537
  }
1575
1538
 
1576
- // src/commands/templates/render-source.ts
1577
- var HTML_EXTENSIONS = [".html", ".htm"];
1578
- var TEXT_EXTENSIONS = [".txt", ".text"];
1579
- var MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx"];
1580
- var TYPESCRIPT_EXTENSIONS = [".ts", ".tsx", ".jsx"];
1581
- function extensionOf(filePath) {
1582
- const match = /\.[^.\/\\]+$/.exec(filePath);
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
- async function defaultReadFile(filePath) {
1586
- const { readFile: readFile3 } = await import("fs/promises");
1587
- return readFile3(filePath, "utf8");
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
- async function defaultImportModule(filePath) {
1590
- const { pathToFileURL } = await import("url");
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
- async function loadReactEmailRender() {
1610
- try {
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
- async function toElement(mod, props) {
1622
- const record = mod;
1623
- const exported = record?.default ?? mod;
1624
- if (typeof exported === "function") {
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 exported;
1563
+ return { clientId: CLI_OAUTH_CLIENT_ID, scope: input.scope, store: "file", refreshToken: input.refreshToken };
1636
1564
  }
1637
- async function loadTemplateSource(filePath, options = {}, deps = {}) {
1638
- const readFile3 = deps.readFile ?? defaultReadFile;
1639
- const importModule = deps.importModule ?? defaultImportModule;
1640
- const extension = extensionOf(filePath);
1641
- if (HTML_EXTENSIONS.includes(extension)) {
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
- // src/commands/templates/push.ts
1666
- var VALUE_FLAGS = /* @__PURE__ */ new Set([
1667
- "--alias",
1668
- "--subject",
1669
- "--name",
1670
- "--text",
1671
- "--preheader",
1672
- "--props",
1673
- "--api-key",
1674
- "--base-url"
1675
- ]);
1676
- function readPositional(args) {
1677
- for (let index = 0; index < args.length; index += 1) {
1678
- const arg = args[index];
1679
- if (arg.startsWith("--")) {
1680
- if (VALUE_FLAGS.has(arg)) index += 1;
1681
- continue;
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
- return arg;
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 void 0;
1638
+ return { ok: false, error: `The sign-in is no longer valid. ${signInAgain}` };
1686
1639
  }
1687
- function statusOf(error) {
1688
- const status = error?.status;
1689
- return typeof status === "number" ? status : void 0;
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
- async function runPushCommand(args, env = process.env, deps = {}) {
1692
- const options = parseTemplatesCommonArgs(args);
1693
- const apiKey = resolveApiKey7(options, env);
1694
- if (!apiKey) {
1695
- return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
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
- const file = readFlag19(args, "--file") ?? readPositional(args);
1698
- if (!file) {
1699
- return { exitCode: 1, error: "Missing template file. Usage: templates push <file> --alias <alias>" };
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
- const alias = readFlag19(args, "--alias");
1702
- if (!alias) {
1703
- return { exitCode: 1, error: "Missing --alias. The alias identifies which template to update." };
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
- const subject = readFlag19(args, "--subject");
1706
- const name = readFlag19(args, "--name");
1707
- const preheader = readFlag19(args, "--preheader");
1708
- const textFile = readFlag19(args, "--text");
1709
- const propsRaw = readFlag19(args, "--props");
1710
- const shouldPublish = args.includes("--publish");
1711
- let props = {};
1712
- if (propsRaw) {
1713
- try {
1714
- const parsed = JSON.parse(propsRaw);
1715
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1716
- throw new Error("not an object");
1717
- }
1718
- props = parsed;
1719
- } catch {
1720
- return { exitCode: 1, error: `--props must be a JSON object, e.g. --props '{"name":"Ada"}'` };
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
- try {
1724
- const source = await loadTemplateSource(file, { props }, deps);
1725
- if (textFile) {
1726
- const textSource = await loadTemplateSource(textFile, { props }, deps);
1727
- source.text = textSource.text ?? textSource.html;
1728
- }
1729
- if (!source.html && !source.text) {
1730
- return { exitCode: 1, error: `${file} produced no content to push.` };
1731
- }
1732
- const createClient2 = deps.createClient ?? createTemplatesClient;
1733
- const client = await createClient2(apiKey, options.baseUrl);
1734
- let existed = true;
1735
- try {
1736
- await client.templates.get(alias);
1737
- } catch (error) {
1738
- if (statusOf(error) !== 404) throw error;
1739
- existed = false;
1740
- }
1741
- const body = {
1742
- format: "html",
1743
- ...source.html ? { html: source.html } : {},
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/commands/templates/create.ts
1777
- async function runCreateCommand3(args, env = process.env) {
1778
- const options = parseTemplatesCommonArgs(args);
1779
- const apiKey = resolveApiKey7(options, env);
1780
- if (!apiKey) {
1781
- return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
1782
- }
1783
- const name = readFlag19(args, "--name");
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
- if (!html && !text) {
1797
- return { exitCode: 1, error: 'Missing body. Pass --html "<p>Hi</p>" or --text "Hi"' };
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 client = await createTemplatesClient(apiKey, options.baseUrl);
1801
- const result = await client.templates.create({
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
- console.log(JSON.stringify(result));
1811
- return { exitCode: 0 };
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
- // src/commands/templates/update.ts
1818
- var POSITIONAL_FLAGS11 = [
1819
- "--api-key",
1820
- "--base-url",
1821
- "--id",
1822
- "--alias",
1823
- "--set-alias",
1824
- "--name",
1825
- "--subject",
1826
- "--html",
1827
- "--text",
1828
- "--variable"
1829
- ];
1830
- async function runUpdateCommand4(args, env = process.env) {
1831
- const options = parseTemplatesCommonArgs(args);
1832
- const apiKey = resolveApiKey7(options, env);
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 idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional12(args);
1837
- if (!idOrAlias) {
1838
- return { exitCode: 1, error: "Missing template id or alias. Pass --id tpl_\u2026 or --alias welcome" };
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
- const name = readFlag19(args, "--name");
1841
- const subject = readFlag19(args, "--subject");
1842
- const html = readFlag19(args, "--html");
1843
- const text = readFlag19(args, "--text");
1844
- const alias = readFlag19(args, "--set-alias");
1845
- const variables = readFlags3(args, "--variable");
1846
- const category = readFlag19(args, "--category");
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
- const result = await client.templates.update(idOrAlias, {
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 readFlags3(args, flag) {
1875
- const values = [];
2325
+ function readFirstPositional8(args) {
1876
2326
  for (let i = 0; i < args.length; i += 1) {
1877
- if (args[i] === flag && args[i + 1]) values.push(args[i + 1]);
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 values;
2334
+ return void 0;
1880
2335
  }
1881
- function readFirstPositional12(args) {
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 (POSITIONAL_FLAGS11.includes(arg)) i += 1;
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/from-starter.ts
1894
- var POSITIONAL_FLAGS12 = ["--api-key", "--base-url", "--alias"];
1895
- function readFirstPositional13(args) {
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 (POSITIONAL_FLAGS12.includes(arg)) i += 1;
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
- async function runFromStarterCommand(args, env = process.env) {
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
- console.log(JSON.stringify(await client.templates.fromStarter(alias)));
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
- async function runListStartersCommand(args, env = process.env) {
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
- return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
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
- const client = await createTemplatesClient(apiKey, options.baseUrl);
1935
- console.log(JSON.stringify(await client.templates.listStarters()));
1936
- return { exitCode: 0 };
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/versions.ts
1943
- var POSITIONAL_FLAGS13 = ["--api-key", "--base-url", "--id", "--alias", "--version", "--from", "--to"];
1944
- function readFirstPositional14(args) {
1945
- for (let i = 0; i < args.length; i += 1) {
1946
- const arg = args[i];
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 (POSITIONAL_FLAGS13.includes(arg)) i += 1;
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
- async function runVersionsCommand(args, env = process.env) {
1956
- const options = parseTemplatesCommonArgs(args);
1957
- const apiKey = resolveApiKey7(options, env);
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 runTestSendCommand(args, env = process.env) {
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 idOrAlias = readFlag19(args, "--id") || readFlag19(args, "--alias") || readFirstPositional14(args);
2003
- const from = readFlag19(args, "--from");
2004
- const to = readFlag19(args, "--to");
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
- try {
2079
- const client = await createIntegrationsClient(apiKey, options.baseUrl);
2080
- const result = await client.integrations.connectTwilio({
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
- async function runTwilioDisconnectCommand(args, env = process.env) {
2093
- const options = parseIntegrationsCommonArgs(args);
2094
- const apiKey = resolveApiKey8(options, env);
2095
- if (!apiKey) {
2096
- return { exitCode: 1, error: "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key." };
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 client = await createIntegrationsClient(apiKey, options.baseUrl);
2100
- const result = await client.integrations.disconnectTwilio();
2101
- console.log(JSON.stringify(result));
2102
- return { exitCode: 0 };
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 (!arg.startsWith("--") || arg === "-") {
2127
- positionals.push(arg);
2128
- continue;
2599
+ if (!source.html && !source.text) {
2600
+ return { exitCode: 1, error: `${file} produced no content to push.` };
2129
2601
  }
2130
- const eq = arg.indexOf("=");
2131
- const name = eq === -1 ? arg : arg.slice(0, eq);
2132
- if (valueFlags.has(name)) {
2133
- const value = eq === -1 ? argv[i + 1] : arg.slice(eq + 1);
2134
- if (value === void 0 || eq === -1 && value.startsWith("--")) {
2135
- return { ok: false, error: `${name} needs a value.` };
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
- values[name] = value;
2138
- if (eq === -1) i++;
2139
- continue;
2140
- }
2141
- if (booleanFlags.has(name) && eq === -1) {
2142
- booleans[name] = true;
2143
- continue;
2629
+ result = await client.templates.create({
2630
+ name: name || alias,
2631
+ subject,
2632
+ alias,
2633
+ ...body
2634
+ });
2144
2635
  }
2145
- const negated = name.startsWith("--no-") ? `--${name.slice("--no-".length)}` : null;
2146
- if (negated && booleanFlags.has(negated) && eq === -1) {
2147
- booleans[negated] = false;
2148
- continue;
2636
+ if (shouldPublish) {
2637
+ result = await client.templates.publish(alias);
2149
2638
  }
2150
- const known = [...valueFlags, ...booleanFlags].sort().join(", ");
2151
- return { ok: false, error: `Unknown flag ${name}.${known ? ` This command takes: ${known}.` : ""}` };
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/session/config.ts
2187
- import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
2188
- import { homedir } from "os";
2189
- import { dirname, join } from "path";
2190
- function configDir(env = process.env) {
2191
- const override = env.GROUNDMAIL_CONFIG_DIR?.trim();
2192
- if (override) return override;
2193
- const base = env.XDG_CONFIG_HOME?.trim() || (process.platform === "win32" ? env.APPDATA?.trim() || join(homedir(), "AppData", "Roaming") : "") || join(homedir(), ".config");
2194
- return join(base, cliBrand(env).configDirName);
2195
- }
2196
- function configPath(env = process.env) {
2197
- return join(configDir(env), "config.json");
2198
- }
2199
- function loadConfig(env = process.env) {
2200
- try {
2201
- const parsed = JSON.parse(readFileSync(configPath(env), "utf8"));
2202
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
2203
- const record = parsed;
2204
- const workspaces = {};
2205
- for (const [id, entry] of Object.entries(record.workspaces ?? {})) {
2206
- if (entry && typeof entry.apiKey === "string" && entry.apiKey && typeof entry.id === "string") {
2207
- workspaces[id] = entry;
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
- chmodSync(file, 384);
2228
- } catch {
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/session/credentials.ts
2250
- function envApiKey(env = process.env) {
2251
- return env.GROUNDMAIL_API_KEY?.trim() || null;
2252
- }
2253
- function currentSavedWorkspace(config, env = process.env) {
2254
- const brand = cliBrand(env);
2255
- const all = savedWorkspaces(config);
2256
- if (config.current && config.workspaces?.[config.current]) {
2257
- return { ok: true, workspace: config.workspaces[config.current] };
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
- if (all.length === 1) return { ok: true, workspace: all[0] };
2260
- if (all.length === 0) {
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
- return {
2268
- ok: false,
2269
- reason: "ambiguous",
2270
- 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>.`
2271
- };
2272
- }
2273
- function resolveAuth(flags, env = process.env, config = loadConfig(env)) {
2274
- const brand = cliBrand(env);
2275
- const flagUrl = trimUrl(flags.baseUrl) || null;
2276
- const urlFor = (workspace) => flagUrl || explicitApiUrl(env) || trimUrl(workspace?.apiUrl) || defaultApiUrl(env);
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
- ok: true,
2290
- auth: { apiKey: match.workspace.apiKey, baseUrl: urlFor(match.workspace), source: "saved", workspace: match.workspace }
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
- const fromEnv = envApiKey(env);
2294
- if (fromEnv) return { ok: true, auth: { apiKey: fromEnv, baseUrl: urlFor(null), source: "env", workspace: null } };
2295
- const current = currentSavedWorkspace(config, env);
2296
- if (!current.ok) return { ok: false, error: current.error };
2297
- return {
2298
- ok: true,
2299
- auth: { apiKey: current.workspace.apiKey, baseUrl: urlFor(current.workspace), source: "saved", workspace: current.workspace }
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 applySavedSession(argv, env = process.env, config) {
2303
- const hasFlag = (flag) => argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
2304
- let workspace = null;
2305
- let notice;
2306
- if (!hasFlag("--api-key") && !envApiKey(env)) {
2307
- const current = currentSavedWorkspace(config ?? loadConfig(env), env);
2308
- if (current.ok) {
2309
- workspace = current.workspace;
2310
- env.GROUNDMAIL_API_KEY = current.workspace.apiKey;
2311
- } else if (current.reason === "ambiguous") {
2312
- notice = current.error;
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
- if (hasFlag("--base-url")) return { argv, notice };
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
- // package.json
2321
- var version = "0.15.0";
2322
-
2323
- // src/user-agent.ts
2324
- function buildUserAgent(own) {
2325
- return `${own.name}/${own.version} node/${process.versions.node}`;
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 withUserAgent(init, userAgent) {
2328
- const headers = init?.headers;
2329
- if (headers instanceof Headers) {
2330
- const next = new Headers(headers);
2331
- if (!next.has("user-agent")) next.set("User-Agent", userAgent);
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
- if (Array.isArray(headers)) {
2335
- const has2 = headers.some(([key]) => key.toLowerCase() === "user-agent");
2336
- return { ...init, headers: has2 ? headers : [...headers, ["User-Agent", userAgent]] };
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
- response = await doFetch(url, {
2358
- method: options.method ?? (options.body === void 0 ? "GET" : "POST"),
2359
- headers: {
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
- const reason = error instanceof Error ? error.message : String(error);
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 errorMessage(response, fallback) {
2384
- const error = response.body.error;
2385
- if (typeof error === "string" && error.trim()) return error.trim();
2386
- if (error && typeof error === "object") {
2387
- const message = error.message;
2388
- if (typeof message === "string" && message.trim()) return message.trim();
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/session/io.ts
2394
- var processIo = {
2395
- out: (text) => process.stdout.write(text.endsWith("\n") ? text : `${text}
2396
- `),
2397
- err: (text) => process.stderr.write(text.endsWith("\n") ? text : `${text}
2398
- `),
2399
- get isTTY() {
2400
- return Boolean(process.stdout.isTTY);
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
- // src/commands/login.ts
2408
- var realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2409
- function openInBrowser(url) {
2410
- const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
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 child = spawn(cmd, args, { stdio: "ignore", detached: true });
2413
- child.on("error", () => {
2414
- });
2415
- child.unref();
2416
- } catch {
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 sourceLabel(source, env) {
2420
- if (source === "flag") return "--api-key";
2421
- if (source === "env") return cliBrand(env).keyEnvVar;
2422
- return "saved login";
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 json = wantsJson(args, io);
2441
- const wait = args.booleans["--wait"] ?? io.isTTY;
2442
- const browser = args.booleans["--browser"] ?? true;
2443
- const apiUrl = trimUrl(args.values["--base-url"]) || explicitApiUrl(env) || defaultApiUrl(env);
2444
- const workspaceHint = args.values["--workspace"]?.trim() || null;
2445
- try {
2446
- const config = loadConfig(env);
2447
- let login = config.pendingLogin;
2448
- const resumed = Boolean(
2449
- !args.booleans["--new"] && login && login.apiUrl === apiUrl && login.expiresAt > now() + 5e3 && (login.workspace ?? null) === workspaceHint
2450
- );
2451
- if (!resumed && !workspaceHint && !args.booleans["--new"]) {
2452
- const current = currentSavedWorkspace(config, env);
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
- if (error instanceof CliRequestError) return { exitCode: EXIT_FAILED, error: error.message };
2538
- throw error;
2863
+ return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
2539
2864
  }
2540
2865
  }
2541
- function finishLogin(body, login, { io, env, json }) {
2542
- const brand = cliBrand(env);
2543
- const workspace = body.workspace ?? {};
2544
- const user = body.user ?? {};
2545
- const key = body.key ?? {};
2546
- const id = typeof workspace.id === "string" && workspace.id ? workspace.id : "default";
2547
- const saved = {
2548
- id,
2549
- name: typeof workspace.name === "string" && workspace.name ? workspace.name : "Workspace",
2550
- apiKey: body.api_key,
2551
- ...typeof key.id === "string" ? { keyId: key.id } : {},
2552
- ...typeof key.prefix === "string" ? { keyPrefix: key.prefix } : {},
2553
- ...login.apiUrl !== defaultApiUrl(env) ? { apiUrl: login.apiUrl } : {},
2554
- email: typeof user.email === "string" ? user.email : null,
2555
- savedAt: (/* @__PURE__ */ new Date()).toISOString()
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
- const config = updateConfig(({ pendingLogin: _drop, ...rest }) => ({
2558
- ...rest,
2559
- workspaces: { ...rest.workspaces ?? {}, [id]: saved },
2560
- current: id
2561
- }), env);
2562
- const others = savedWorkspaces(config).filter((entry) => entry.id !== id);
2563
- const envKey = envApiKey(env);
2564
- const envNote = envKey ? `${sourceLabel("env", env)} is set in this environment and is used instead of the saved key until you unset it.` : null;
2565
- if (json) {
2566
- printJson(io, {
2567
- status: "signed_in",
2568
- user: body.user ?? null,
2569
- workspace: { id: saved.id, name: saved.name },
2570
- key: { id: saved.keyId ?? null, prefix: saved.keyPrefix ?? null },
2571
- config_path: configPath(env),
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
- const lines = [
2578
- `Signed in${saved.email ? ` as ${saved.email}` : ""} to ${saved.name}. The API key is saved in ${configPath(env)}.`
2579
- ];
2580
- if (others.length) {
2581
- lines.push(
2582
- `Also signed in here: ${others.map((entry) => entry.name).join(", ")}. Switch with ${brand.bin} workspaces use <name>.`
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 runLogoutCommand(argv, deps = {}) {
2591
- const io = deps.io ?? processIo;
2592
- const env = deps.env ?? process.env;
2593
- const brand = cliBrand(env);
2594
- const parsed = parseArgs(argv, { values: ["--workspace", "--base-url"], booleans: ["--json", "--all"] });
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 results = [];
2621
- for (const workspace of targets) {
2622
- const apiUrl = trimUrl(args.values["--base-url"]) || explicitApiUrl(env) || trimUrl(workspace.apiUrl) || defaultApiUrl(env);
2623
- let revoked = false;
2624
- let warning;
2625
- try {
2626
- const response = await apiRequest(apiUrl, "/cli/session", { method: "DELETE", apiKey: workspace.apiKey, env });
2627
- revoked = response.ok && response.body.revoked === true;
2628
- if (!response.ok && response.status !== 401) warning = errorMessage(response, "Could not revoke the key");
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
- const removing = new Set(targets.map((workspace) => workspace.id));
2635
- const hadPending = Boolean(config.pendingLogin);
2636
- if (removing.size || hadPending) {
2637
- updateConfig((current) => {
2638
- const workspaces = Object.fromEntries(
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
- const envNote = envApiKey(env) ? `${sourceLabel("env", env)} is still set in this environment, so commands keep using it.` : null;
2646
- if (json) {
2647
- printJson(io, { signed_out: results, config_path: configPath(env), ...envNote ? { note: envNote } : {} });
2648
- return { exitCode: EXIT_OK };
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
- if (!results.length) {
2651
- io.out(`Not signed in on this computer.${envNote ? ` ${envNote}` : ""}`);
2652
- return { exitCode: EXIT_OK };
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
- async function runWhoamiCommand(argv, deps = {}) {
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: [...COMMON_VALUE_FLAGS], booleans: [...COMMON_BOOLEAN_FLAGS] });
2991
+ const parsed = parseArgs(argv, { values: ["--scope", "--workspace"], booleans: ["--json"] });
2666
2992
  if (!parsed.ok) return { exitCode: EXIT_USAGE, error: `${parsed.error}
2667
- Usage: ${brand.bin} whoami [--workspace <name>] [--json]` };
2993
+ ${usage}` };
2668
2994
  const { args } = parsed;
2669
- const resolved = resolveAuth(
2670
- { apiKey: args.values["--api-key"], baseUrl: args.values["--base-url"], workspace: args.values["--workspace"] },
2671
- env
2672
- );
2673
- if (!resolved.ok) return { exitCode: EXIT_FAILED, error: resolved.error };
2674
- const { auth } = resolved;
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 response = await apiRequest(auth.baseUrl, "/cli/session", { apiKey: auth.apiKey, env });
2677
- if (!response.ok) {
2678
- const hint = response.status === 401 ? ` The key was rejected; run ${brand.bin} login to sign in again.` : "";
2679
- return { exitCode: EXIT_FAILED, error: `${sentence(errorMessage(response, "Could not check the sign-in"))}${hint}` };
2680
- }
2681
- const session = response.body;
2682
- const source = sourceLabel(auth.source, env);
2683
- if (wantsJson(args, io)) {
2684
- printJson(io, { ...response.body, source, api_url: auth.baseUrl, config_path: configPath(env) });
2685
- return { exitCode: EXIT_OK };
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("Usage:");
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
- if (command === "help" || command === "--help" || command === "-h") {
2864
- printUsage();
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
- const session = applySavedSession(process.argv.slice(4));
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") {