groundmail-cli 0.16.4 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +124 -1
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -58,6 +58,7 @@ groundmail domains list
58
58
  groundmail domains verify example.com
59
59
  groundmail webhooks list
60
60
  groundmail emails list
61
+ SENDGRID_API_KEY=… groundmail migrate sendgrid --dry-run # import from SendGrid, Postmark or Mailgun (beta)
61
62
  ```
62
63
 
63
64
  Run `groundmail --help` or `groundmail <command> --help` for flags.
package/dist/cli.js CHANGED
@@ -1288,7 +1288,7 @@ function trimUrl(value) {
1288
1288
  }
1289
1289
 
1290
1290
  // package.json
1291
- var version = "0.16.4";
1291
+ var version = "0.17.0";
1292
1292
 
1293
1293
  // src/user-agent.ts
1294
1294
  function buildUserAgent(own) {
@@ -3111,6 +3111,124 @@ async function switchWorkspace(argv, deps) {
3111
3111
  return { exitCode: EXIT_OK };
3112
3112
  }
3113
3113
 
3114
+ // src/commands/migrate.ts
3115
+ var PROVIDERS = ["sendgrid", "postmark", "mailgun"];
3116
+ var RESOURCES = ["domains", "webhooks", "templates", "suppressions"];
3117
+ var KEY_ENV = {
3118
+ sendgrid: "SENDGRID_API_KEY",
3119
+ postmark: "POSTMARK_SERVER_TOKEN",
3120
+ mailgun: "MAILGUN_API_KEY"
3121
+ };
3122
+ var LABELS = { sendgrid: "SendGrid", postmark: "Postmark", mailgun: "Mailgun" };
3123
+ function readFlag21(args, flag) {
3124
+ const index = args.indexOf(flag);
3125
+ if (index === -1) return void 0;
3126
+ return args[index + 1];
3127
+ }
3128
+ function n(value) {
3129
+ return (value ?? 0).toLocaleString("en-US");
3130
+ }
3131
+ function formatMigrationSummary(m) {
3132
+ const label = LABELS[m.provider];
3133
+ const lines = [
3134
+ m.dry_run ? `Preview of your ${label} import (nothing written):` : `Imported from ${label} (${m.status}):`
3135
+ ];
3136
+ if (m.domains) {
3137
+ lines.push(` Domains ${n(m.domains.totals.imported)} of ${n(m.domains.totals.found)}`);
3138
+ }
3139
+ if (m.webhooks) {
3140
+ lines.push(` Webhooks ${n(m.webhooks.totals.imported)} of ${n(m.webhooks.totals.found)} (created disabled)`);
3141
+ }
3142
+ if (m.templates) {
3143
+ const t = m.templates.totals;
3144
+ lines.push(` Templates ${n(t.imported)} ready, ${n(t.needs_attention)} to review, of ${n(t.found)}`);
3145
+ }
3146
+ if (m.suppressions) {
3147
+ const s = m.suppressions.totals;
3148
+ lines.push(
3149
+ m.dry_run ? ` Suppressions ${n(s.found)}${m.suppressions.estimated ? "+ (first page of each list)" : ""}` : ` Suppressions ${n(s.imported)} added, ${n(s.already_suppressed)} already suppressed`
3150
+ );
3151
+ }
3152
+ if (m.errors.length) {
3153
+ lines.push("", "Could not read:");
3154
+ for (const e of m.errors) lines.push(` ${e.resource}: ${e.message}`);
3155
+ }
3156
+ const failed = [
3157
+ ...(m.domains?.items ?? []).filter((i) => i.status === "failed").map((i) => `domain ${i.name}: ${i.message ?? "failed"}`),
3158
+ ...(m.webhooks?.items ?? []).filter((i) => i.status === "failed").map((i) => `webhook ${i.url}: ${i.message ?? "failed"}`),
3159
+ ...(m.templates?.items ?? []).filter((i) => i.status === "failed").map((i) => `template ${i.name}: ${i.message ?? "failed"}`)
3160
+ ];
3161
+ if (failed.length) {
3162
+ lines.push("", "Not imported:");
3163
+ for (const line of failed.slice(0, 20)) lines.push(` ${line}`);
3164
+ if (failed.length > 20) lines.push(` \u2026and ${failed.length - 20} more (see the JSON report)`);
3165
+ }
3166
+ if (m.attention.length) {
3167
+ lines.push("", "Next:");
3168
+ for (const line of m.attention) lines.push(` - ${line}`);
3169
+ }
3170
+ const node = m.code_changes?.find((c) => c.stack === "node");
3171
+ if (node) lines.push("", "Code change (Node.js):", node.diff);
3172
+ if (m.agent_prompt) lines.push("", "Prompt for your coding agent is in the JSON report (`agent_prompt`).");
3173
+ return lines.join("\n");
3174
+ }
3175
+ async function runMigrateCommand(provider, args, env = process.env) {
3176
+ if (!provider || !PROVIDERS.includes(provider)) {
3177
+ return { exitCode: 1, error: "Usage: groundmail migrate <sendgrid|postmark|mailgun> [--dry-run] [--only templates,suppressions]" };
3178
+ }
3179
+ const id = provider;
3180
+ const apiKey = readFlag21(args, "--api-key") || env.GROUNDMAIL_API_KEY;
3181
+ if (!apiKey) {
3182
+ return { exitCode: 1, error: "Not signed in. Run `groundmail login`, or set GROUNDMAIL_API_KEY." };
3183
+ }
3184
+ const providerKey = readFlag21(args, "--key") || env[KEY_ENV[id]];
3185
+ if (!providerKey) {
3186
+ return {
3187
+ exitCode: 1,
3188
+ error: `Missing ${LABELS[id]} key. Set ${KEY_ENV[id]} (or pass --key). It is used for this import only and never stored.`
3189
+ };
3190
+ }
3191
+ const params = { provider: id, api_key: providerKey };
3192
+ if (id === "postmark") {
3193
+ const accountToken = readFlag21(args, "--account-token") || env.POSTMARK_ACCOUNT_TOKEN;
3194
+ if (accountToken) params.account_api_key = accountToken;
3195
+ } else {
3196
+ const region = readFlag21(args, "--region");
3197
+ if (region === "us" || region === "eu") params.region = region;
3198
+ else if (region) return { exitCode: 1, error: "--region must be us or eu" };
3199
+ }
3200
+ const only = readFlag21(args, "--only");
3201
+ if (only) {
3202
+ const list = only.split(",").map((s) => s.trim()).filter(Boolean);
3203
+ const bad = list.filter((r) => !RESOURCES.includes(r));
3204
+ if (bad.length || !list.length) {
3205
+ return { exitCode: 1, error: `--only takes a comma-separated list of: ${RESOURCES.join(", ")}` };
3206
+ }
3207
+ params.resources = list;
3208
+ }
3209
+ if (args.includes("--dry-run")) params.dry_run = true;
3210
+ try {
3211
+ const { Groundmail } = await import("groundmail");
3212
+ const baseUrl = readFlag21(args, "--base-url");
3213
+ const client = new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
3214
+ console.error(`${params.dry_run ? "Previewing" : "Importing"} from ${LABELS[id]} (Beta)\u2026`);
3215
+ const report = await client.migrations.run(params, {
3216
+ onProgress: (m) => {
3217
+ if (m.has_more) {
3218
+ const s = m.suppressions?.totals;
3219
+ console.error(` \u2026still working${s ? ` (${n(s.found)} suppressions read)` : ""}`);
3220
+ }
3221
+ }
3222
+ });
3223
+ console.log(JSON.stringify(report));
3224
+ console.error("");
3225
+ console.error(formatMigrationSummary(report));
3226
+ return { exitCode: report.status === "failed" ? 1 : 0 };
3227
+ } catch (error) {
3228
+ return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
3229
+ }
3230
+ }
3231
+
3114
3232
  // src/help.ts
3115
3233
  var USAGE_LINES = [
3116
3234
  " groundmail login [--scope <scopes>] [--workspace <name>] sign in through the browser (OAuth; no key to copy)",
@@ -3171,6 +3289,9 @@ var USAGE_LINES = [
3171
3289
  " groundmail keys list",
3172
3290
  " groundmail keys create [--name Default] [--scopes emails:send,domains:read] [--allowed-domains example.com]",
3173
3291
  " groundmail keys delete --id <api-key-id>",
3292
+ " groundmail migrate sendgrid [--dry-run] [--only templates,suppressions,domains,webhooks] [--region eu] import from SendGrid (Beta)",
3293
+ " groundmail migrate postmark [--dry-run] [--only \u2026] import from Postmark (Beta)",
3294
+ " groundmail migrate mailgun [--dry-run] [--only \u2026] [--region eu] import from Mailgun (Beta)",
3174
3295
  " groundmail integrations twilio status",
3175
3296
  " groundmail integrations twilio connect --account-sid AC\u2026 --auth-token \u2026 --from +15551234567",
3176
3297
  " groundmail integrations twilio disconnect"
@@ -3183,6 +3304,7 @@ var FOOTER_LINES = [
3183
3304
  " CLOUDFLARE_API_TOKEN=\u2026 (optional local apply; else uses dashboard Integrations)",
3184
3305
  " VERCEL_API_TOKEN=\u2026 VERCEL_TEAM_ID=\u2026 (optional local apply; else uses dashboard Integrations)",
3185
3306
  " GODADDY_API_KEY=\u2026 GODADDY_API_SECRET=\u2026 (domains apply --provider godaddy)",
3307
+ " SENDGRID_API_KEY / POSTMARK_SERVER_TOKEN (+ POSTMARK_ACCOUNT_TOKEN) / MAILGUN_API_KEY (migrate; used once, never stored)",
3186
3308
  "",
3187
3309
  "Scopes: full, emails:send, emails:read, domains:read, domains:write, webhooks:write, templates:write,",
3188
3310
  " suppressions:write, account:read",
@@ -3258,6 +3380,7 @@ async function main() {
3258
3380
  const session = await applySavedSession(process.argv.slice(4));
3259
3381
  if (session.notice) console.error(session.notice);
3260
3382
  const rest = session.argv;
3383
+ if (command === "migrate") await finish(await runMigrateCommand(subcommand, rest));
3261
3384
  if (command === "domains") {
3262
3385
  if (subcommand === "add" || subcommand === "create") {
3263
3386
  const result = await runAddCommand(rest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundmail-cli",
3
- "version": "0.16.4",
3
+ "version": "0.17.0",
4
4
  "description": "Groundmail CLI — send transactional email from the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  "prepublishOnly": "npm run build"
39
39
  },
40
40
  "dependencies": {
41
- "groundmail": "0.14.5"
41
+ "groundmail": "0.14.6"
42
42
  },
43
43
  "devDependencies": {
44
44
  "tsup": "^8.5.0",