@ascendkit/cli 0.3.1 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -20,6 +20,7 @@ import { redactArgs } from "./utils/redaction.js";
20
20
  import { captureTelemetry } from "./utils/telemetry.js";
21
21
  import { hostname as osHostname, platform as osPlatform } from "node:os";
22
22
  import { formatJourneyAnalytics, formatJourneyWithGuidance, formatNodeList, formatSingleNode, formatSingleTransition, formatTransitionList, } from "./utils/journey-format.js";
23
+ import { formatDistributionResult, formatQuestionList, formatSingleQuestion, } from "./utils/survey-format.js";
23
24
  const require = createRequire(import.meta.url);
24
25
  const { version: CLI_VERSION } = require("../package.json");
25
26
  const HELP = `ascendkit v${CLI_VERSION} - AscendKit CLI
@@ -68,10 +69,10 @@ Commands:
68
69
  template: `Usage: ascendkit template <command>
69
70
 
70
71
  Commands:
71
- template create --name <name> --subject <subject> --body-html <html> --body-text <text> [--slug <slug>] [--description <description>]
72
+ template create --name <name> --subject <subject> --body-html <html> --body-text <text> [--slug <slug>] [--description <description>] [--category <marketing|transactional>]
72
73
  template list [--query <search>] [--system true|--custom true]
73
74
  template show <template-id>
74
- template update <template-id> [--subject <subject>] [--body-html <html>] [--body-text <text>] [--change-note <note>]
75
+ template update <template-id> [--subject <subject>] [--body-html <html>] [--body-text <text>] [--change-note <note>] [--category <marketing|transactional>]
75
76
  template remove <template-id>
76
77
  template version list <template-id>
77
78
  template version show <template-id> <n>`,
@@ -102,16 +103,16 @@ Commands:
102
103
  journey update <journey-id> [--name <name>] [--nodes <json>] [--transitions <json>] [--description <description>] [--entry-event <event>] [--entry-node <node>] [--entry-conditions <json>] [--re-entry-policy <skip|restart>]
103
104
  journey remove <journey-id>
104
105
  journey activate <journey-id>
105
- journey pause <journey-id>
106
+ journey pause <journey-id> [--yes]
106
107
  journey resume <journey-id>
107
108
  journey archive <journey-id>
108
109
  journey analytics <journey-id>
109
110
  journey node list <journey-id>
110
- journey node add <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
111
+ journey node add <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>] [--quiet]
111
112
  journey node update <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
112
113
  journey node remove <journey-id> <node-name>
113
114
  journey transition list <journey-id> [--from <node-name>] [--to <node-name>]
114
- journey transition add <journey-id> --from <node-name> --to <node-name> --trigger <json> [--priority <n>] [--name <transition-name>]
115
+ journey transition add <journey-id> --from <node-name> --to <node-name> --trigger <json> [--priority <n>] [--name <transition-name>] [--quiet]
115
116
  journey transition update <journey-id> <transition-name> [--trigger <json>] [--priority <n>]
116
117
  journey transition remove <journey-id> <transition-name>`,
117
118
  "email-identity": `Usage: ascendkit email-identity <command>
@@ -123,6 +124,7 @@ Commands:
123
124
  email-identity remove-domain
124
125
  email-identity list
125
126
  email-identity add <email> [--display-name <name>]
127
+ email-identity update <email> --display-name <name>
126
128
  email-identity resend <email>
127
129
  email-identity set-default <email> [--display-name <name>]
128
130
  email-identity remove <email>
@@ -291,6 +293,8 @@ function printTemplateSummary(data, opts) {
291
293
  console.log(`Template: ${data.name} (${data.id})`);
292
294
  if (data.slug)
293
295
  console.log(`Slug: ${data.slug}`);
296
+ if (data.category)
297
+ console.log(`Category: ${data.category}`);
294
298
  console.log(`Subject: ${data.subject ?? "-"}`);
295
299
  if (data.currentVersion != null)
296
300
  console.log(`Version: ${data.currentVersion}`);
@@ -338,6 +342,65 @@ function printSurveySummary(data) {
338
342
  if (questions != null)
339
343
  console.log(`Questions: ${questions}`);
340
344
  }
345
+ function printEnvironmentSummary(data) {
346
+ console.log(`${data.name} (${data.id})`);
347
+ console.log(`Tier: ${data.tier}`);
348
+ if (data.description)
349
+ console.log(`Description: ${data.description}`);
350
+ console.log(`Public key: ${data.publicKey}`);
351
+ if (data.secretKey) {
352
+ const masked = data.secretKey.slice(0, 10) + "****";
353
+ console.log(`Secret key: ${masked}`);
354
+ }
355
+ const vars = data.variables && typeof data.variables === "object" ? Object.entries(data.variables) : [];
356
+ if (vars.length > 0) {
357
+ console.log(`Variables:`);
358
+ for (const [k, v] of vars)
359
+ console.log(` ${k}=${v}`);
360
+ }
361
+ }
362
+ function printPromotionSummary(result) {
363
+ const src = result.source ?? {};
364
+ const tgt = result.target ?? {};
365
+ console.log(`Promoted ${src.tier} → ${tgt.tier} (${tgt.envId})`);
366
+ console.log("");
367
+ const changes = result.changes ?? {};
368
+ const rows = [];
369
+ for (const [key, val] of Object.entries(changes)) {
370
+ const label = key.padEnd(14);
371
+ if (val.action === "unchanged") {
372
+ rows.push([label, "unchanged"]);
373
+ }
374
+ else if (val.action === "skip") {
375
+ rows.push([label, `skipped — ${val.reason}`]);
376
+ }
377
+ else if (val.action === "update" || val.action === "add_only") {
378
+ const fields = Array.isArray(val.fields) && val.fields.length ? ` (${val.fields.join(", ")})` : "";
379
+ rows.push([label, `updated${fields}`]);
380
+ }
381
+ else if (val.items) {
382
+ const counts = Object.entries(val.counts ?? {}).map(([k, v]) => `${v} ${k}`).join(", ");
383
+ rows.push([label, counts || "unchanged"]);
384
+ }
385
+ else {
386
+ rows.push([label, val.action ?? "—"]);
387
+ }
388
+ }
389
+ for (const [label, status] of rows) {
390
+ console.log(` ${label} ${status}`);
391
+ }
392
+ // Only show warnings not already conveyed by a skipped row in the table
393
+ const skippedKeys = new Set(Object.entries(changes)
394
+ .filter(([, v]) => v.action === "skip")
395
+ .map(([k]) => k.toUpperCase()));
396
+ const warnings = (Array.isArray(result.warnings) ? result.warnings : [])
397
+ .filter((w) => !skippedKeys.size || !Array.from(skippedKeys).some(k => w.code?.toUpperCase().includes(k)));
398
+ if (warnings.length > 0) {
399
+ console.log("");
400
+ for (const w of warnings)
401
+ console.log(`⚠ ${w.message}`);
402
+ }
403
+ }
341
404
  function printProjectSummary(data) {
342
405
  console.log(`Project: ${data.id}`);
343
406
  console.log(`Name: ${data.name}`);
@@ -348,6 +411,92 @@ function printProjectSummary(data) {
348
411
  console.log(`Environment: ${data.environment.publicKey}`);
349
412
  }
350
413
  }
414
+ function printCampaignSummary(data) {
415
+ console.log(`Campaign: ${data.name} (${data.id})`);
416
+ if (data.templateName)
417
+ console.log(`Template: ${data.templateName}`);
418
+ console.log(`Status: ${data.status} | Recipients: ${data.totalRecipients ?? 0}`);
419
+ if (data.audienceSummary)
420
+ console.log(`Audience: ${data.audienceSummary}`);
421
+ if (data.scheduledAt)
422
+ console.log(`Scheduled: ${data.scheduledAt}`);
423
+ if (data.sentAt)
424
+ console.log(`Sent: ${data.sentAt}`);
425
+ if (data.fromIdentityEmail)
426
+ console.log(`From: ${data.fromIdentityEmail}`);
427
+ }
428
+ function printCampaignAnalytics(data) {
429
+ const sent = data.sent ?? 0;
430
+ const opened = data.opened ?? 0;
431
+ const clicked = data.clicked ?? 0;
432
+ const bounced = data.bounced ?? 0;
433
+ const rates = data.rates ?? {};
434
+ const fmtRate = (r) => r != null ? ` (${Math.round(r * 100)}%)` : "";
435
+ console.log(`Sent: ${sent} | Opened: ${opened}${fmtRate(rates.openRate)} | Clicked: ${clicked}${fmtRate(rates.clickRate)} | Bounced: ${bounced}`);
436
+ }
437
+ function printAudiencePreview(data) {
438
+ const count = data.count ?? data.total ?? 0;
439
+ console.log(`Matched ${count} recipient(s)`);
440
+ }
441
+ function printWebhookSummary(data, opts) {
442
+ console.log(`Webhook: ${data.id}`);
443
+ const events = Array.isArray(data.events) && data.events.length > 0 ? data.events.join(", ") : "all";
444
+ console.log(`URL: ${data.url} | Status: ${data.status}`);
445
+ console.log(`Events: ${events}`);
446
+ if (opts?.showSecret && data.secret) {
447
+ console.log(`Secret: ${data.secret} (save this — it won't be shown again)`);
448
+ }
449
+ }
450
+ function printWebhookTestResult(data) {
451
+ if (data.success) {
452
+ console.log(`✓ Delivered: HTTP ${data.statusCode}`);
453
+ }
454
+ else if (data.statusCode) {
455
+ console.log(`✗ Failed: HTTP ${data.statusCode}`);
456
+ }
457
+ else {
458
+ console.log(`✗ Failed`);
459
+ }
460
+ if (data.error)
461
+ console.log(data.error);
462
+ }
463
+ function printTemplateVersionSummary(data) {
464
+ const note = data.changeNote || "no note";
465
+ console.log(`Version ${data.versionNumber} — ${note}`);
466
+ console.log(`Subject: ${data.subject ?? "—"}`);
467
+ if (Array.isArray(data.variables) && data.variables.length > 0) {
468
+ console.log(`Variables: ${data.variables.join(", ")}`);
469
+ }
470
+ if (data.createdAt) {
471
+ console.log(`Created: ${data.createdAt}`);
472
+ }
473
+ if (data.bodyHtml) {
474
+ console.log(`\n--- HTML Body ---\n${data.bodyHtml}`);
475
+ }
476
+ if (data.bodyText) {
477
+ console.log(`\n--- Plain Text Body ---\n${data.bodyText}`);
478
+ }
479
+ }
480
+ function printSurveyAnalytics(data) {
481
+ const funnel = data.funnel ?? {};
482
+ const invited = funnel.sent ?? data.totalInvited ?? 0;
483
+ const responded = funnel.submitted ?? data.totalResponded ?? 0;
484
+ const rate = funnel.completionRate ?? data.completionRate ?? 0;
485
+ console.log(`Responses: ${responded}/${invited} (${rate}%)`);
486
+ if (data.npsScore != null)
487
+ console.log(`NPS: ${data.npsScore}`);
488
+ if (data.csatAverage != null)
489
+ console.log(`CSAT: ${data.csatAverage}`);
490
+ }
491
+ function printSurveyInvitations(data) {
492
+ const rows = Array.isArray(data) ? data : [];
493
+ table(rows, [
494
+ { key: "id", label: "ID", width: 20 },
495
+ { key: "userId", label: "User", width: 20 },
496
+ { key: "status", label: "Status", width: 12 },
497
+ { key: "sentAt", label: "Sent At", width: 22 },
498
+ ]);
499
+ }
351
500
  function normalizeJourneyRows(data) {
352
501
  if (Array.isArray(data)) {
353
502
  return data;
@@ -383,6 +532,16 @@ function table(rows, columns) {
383
532
  console.log(line);
384
533
  }
385
534
  }
535
+ function normalizeTemplateCategoryFlag(value) {
536
+ if (value === undefined)
537
+ return undefined;
538
+ if (typeof value != "string")
539
+ return undefined;
540
+ const normalized = value.trim().toLowerCase();
541
+ if (normalized === "marketing" || normalized === "transactional")
542
+ return normalized;
543
+ return undefined;
544
+ }
386
545
  async function run() {
387
546
  installGlobalHandlers();
388
547
  const args = process.argv.slice(2);
@@ -447,6 +606,14 @@ async function run() {
447
606
  }
448
607
  return;
449
608
  }
609
+ // --help anywhere after the action (e.g. ascendkit template update <id> --help)
610
+ if (args.slice(2).some(a => a === "--help" || a === "-h")) {
611
+ if (!printSectionHelp(domain)) {
612
+ console.error(`Unknown command section: ${domain}`);
613
+ return await exitCli(1);
614
+ }
615
+ return;
616
+ }
450
617
  // Platform commands (don't need environment key)
451
618
  switch (domain) {
452
619
  case "init": {
@@ -756,7 +923,7 @@ async function runEnvironment(action, rest) {
756
923
  }
757
924
  switch (action) {
758
925
  case "show":
759
- output(await platform.getEnvironment(ctx.projectId, ctx.environmentId));
926
+ printEnvironmentSummary(await platform.getEnvironment(ctx.projectId, ctx.environmentId));
760
927
  return;
761
928
  case "promote": {
762
929
  const envId = rest[0] && !rest[0].startsWith("--") ? rest[0] : ctx.environmentId;
@@ -767,8 +934,7 @@ async function runEnvironment(action, rest) {
767
934
  }
768
935
  try {
769
936
  const result = await platform.promoteEnvironment(envId, target);
770
- console.log("Promotion successful:");
771
- console.log(JSON.stringify(result, null, 2));
937
+ printPromotionSummary(result);
772
938
  }
773
939
  catch (err) {
774
940
  let message = err instanceof Error ? err.message : String(err);
@@ -799,7 +965,7 @@ async function runEnvironment(action, rest) {
799
965
  try {
800
966
  const result = await platform.updateEnvironment(ctx.projectId, envId, name, description);
801
967
  console.log("Environment updated:");
802
- console.log(JSON.stringify(result, null, 2));
968
+ printEnvironmentSummary(result);
803
969
  }
804
970
  catch (err) {
805
971
  let message = err instanceof Error ? err.message : String(err);
@@ -1029,6 +1195,16 @@ async function runAuth(client, action, rest) {
1029
1195
  }
1030
1196
  async function runContent(client, action, rest) {
1031
1197
  const flags = parseFlags(rest);
1198
+ // Accept --html/--text as aliases for --body-html/--body-text
1199
+ if (!flags["body-html"] && flags.html)
1200
+ flags["body-html"] = flags.html;
1201
+ if (!flags["body-text"] && flags.text)
1202
+ flags["body-text"] = flags.text;
1203
+ const category = normalizeTemplateCategoryFlag(flags.category);
1204
+ if (flags.category && !category) {
1205
+ console.error("Invalid --category. Use marketing or transactional.");
1206
+ return await exitCli(1);
1207
+ }
1032
1208
  if (!action) {
1033
1209
  console.log(HELP_SECTION.template);
1034
1210
  return;
@@ -1039,13 +1215,14 @@ async function runContent(client, action, rest) {
1039
1215
  switch (normalizedAction) {
1040
1216
  case "create":
1041
1217
  if (!flags.name || !flags.subject || !flags["body-html"] || !flags["body-text"]) {
1042
- console.error("Usage: ascendkit template create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>]");
1218
+ console.error("Usage: ascendkit template create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>] [--category <marketing|transactional>]");
1043
1219
  return await exitCli(1);
1044
1220
  }
1045
1221
  printTemplateSummary(await content.createTemplate(client, {
1046
1222
  name: flags.name, subject: flags.subject,
1047
1223
  bodyHtml: flags["body-html"], bodyText: flags["body-text"],
1048
1224
  slug: flags.slug, description: flags.description,
1225
+ category,
1049
1226
  }));
1050
1227
  break;
1051
1228
  case "list": {
@@ -1061,6 +1238,7 @@ async function runContent(client, action, rest) {
1061
1238
  { key: "id", label: "ID" },
1062
1239
  { key: "name", label: "Name", width: 30 },
1063
1240
  { key: "slug", label: "Slug", width: 25 },
1241
+ { key: "category", label: "Category", width: 14 },
1064
1242
  { key: "subject", label: "Subject", width: 30 },
1065
1243
  ]);
1066
1244
  break;
@@ -1082,6 +1260,7 @@ async function runContent(client, action, rest) {
1082
1260
  bodyHtml: flags["body-html"],
1083
1261
  bodyText: flags["body-text"],
1084
1262
  changeNote: flags["change-note"],
1263
+ category,
1085
1264
  }));
1086
1265
  break;
1087
1266
  case "remove":
@@ -1114,7 +1293,7 @@ async function runContent(client, action, rest) {
1114
1293
  console.error("Usage: ascendkit template version show <template-id> <n>");
1115
1294
  return await exitCli(1);
1116
1295
  }
1117
- output(await content.getVersion(client, templateId, parseInt(versionNumber, 10)));
1296
+ printTemplateVersionSummary(await content.getVersion(client, templateId, parseInt(versionNumber, 10)));
1118
1297
  }
1119
1298
  break;
1120
1299
  default:
@@ -1182,7 +1361,7 @@ async function runSurvey(client, action, rest) {
1182
1361
  console.error("Usage: ascendkit survey distribute <survey-id> --users <usr_id1,usr_id2,...>");
1183
1362
  return await exitCli(1);
1184
1363
  }
1185
- output(await surveys.distributeSurvey(client, rest[0], flags.users.split(",")));
1364
+ console.log(formatDistributionResult(await surveys.distributeSurvey(client, rest[0], flags.users.split(","))));
1186
1365
  break;
1187
1366
  case "invitations":
1188
1367
  case "invitation":
@@ -1192,7 +1371,7 @@ async function runSurvey(client, action, rest) {
1192
1371
  console.error("Usage: ascendkit survey invitation list <survey-id>");
1193
1372
  return await exitCli(1);
1194
1373
  }
1195
- output(await surveys.listInvitations(client, surveyId));
1374
+ printSurveyInvitations(await surveys.listInvitations(client, surveyId));
1196
1375
  }
1197
1376
  else {
1198
1377
  console.error(`Unknown survey invitation command: ${rest[0]}`);
@@ -1204,7 +1383,7 @@ async function runSurvey(client, action, rest) {
1204
1383
  console.error("Usage: ascendkit survey analytics <survey-id>");
1205
1384
  return await exitCli(1);
1206
1385
  }
1207
- output(await surveys.getAnalytics(client, rest[0]));
1386
+ printSurveyAnalytics(await surveys.getAnalytics(client, rest[0]));
1208
1387
  break;
1209
1388
  case "export-definition":
1210
1389
  case "import-definition":
@@ -1269,7 +1448,7 @@ async function runSurvey(client, action, rest) {
1269
1448
  return await exitCli(1);
1270
1449
  }
1271
1450
  if (questionAction === "list") {
1272
- output(await surveys.listQuestions(client, surveyId));
1451
+ console.log(formatQuestionList(await surveys.listQuestions(client, surveyId)));
1273
1452
  }
1274
1453
  else if (questionAction === "add") {
1275
1454
  if (!flags.type || !flags.title) {
@@ -1285,13 +1464,17 @@ async function runSurvey(client, action, rest) {
1285
1464
  params.choices = flags.choices.split(",");
1286
1465
  if (flags.position != null)
1287
1466
  params.position = Number(flags.position);
1288
- output(await surveys.addQuestion(client, surveyId, params));
1467
+ console.log(formatSingleQuestion(await surveys.addQuestion(client, surveyId, params), "Added"));
1289
1468
  }
1290
1469
  else if (questionAction === "update") {
1291
1470
  if (!questionName) {
1292
1471
  console.error("Usage: ascendkit survey question update <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]");
1293
1472
  return await exitCli(1);
1294
1473
  }
1474
+ if (flags.type) {
1475
+ console.error("Error: question type cannot be changed on an existing question. Remove and re-add it to change the type.");
1476
+ return await exitCli(1);
1477
+ }
1295
1478
  const params = {};
1296
1479
  if (flags.title)
1297
1480
  params.title = flags.title;
@@ -1299,21 +1482,23 @@ async function runSurvey(client, action, rest) {
1299
1482
  params.isRequired = flags.required === "true";
1300
1483
  if (flags.choices)
1301
1484
  params.choices = flags.choices.split(",");
1302
- output(await surveys.editQuestion(client, surveyId, questionName, params));
1485
+ console.log(formatSingleQuestion(await surveys.editQuestion(client, surveyId, questionName, params), "Updated"));
1303
1486
  }
1304
1487
  else if (questionAction === "remove") {
1305
1488
  if (!questionName) {
1306
1489
  console.error("Usage: ascendkit survey question remove <survey-id> <question-name>");
1307
1490
  return await exitCli(1);
1308
1491
  }
1309
- output(await surveys.removeQuestion(client, surveyId, questionName));
1492
+ await surveys.removeQuestion(client, surveyId, questionName);
1493
+ console.log("Question removed.");
1310
1494
  }
1311
1495
  else if (questionAction === "reorder") {
1312
1496
  if (!flags.order) {
1313
1497
  console.error("Usage: ascendkit survey question reorder <survey-id> --order <name1,name2,...>");
1314
1498
  return await exitCli(1);
1315
1499
  }
1316
- output(await surveys.reorderQuestions(client, surveyId, flags.order.split(",")));
1500
+ await surveys.reorderQuestions(client, surveyId, flags.order.split(","));
1501
+ console.log("Questions reordered.");
1317
1502
  }
1318
1503
  else {
1319
1504
  console.error(`Unknown survey question command: ${questionAction}`);
@@ -1507,13 +1692,36 @@ async function runJourney(client, action, rest) {
1507
1692
  }
1508
1693
  console.log(formatJourneyWithGuidance(await journeys.activateJourney(client, rest[0])));
1509
1694
  break;
1510
- case "pause":
1695
+ case "pause": {
1511
1696
  if (!rest[0]) {
1512
- console.error("Usage: ascendkit journey pause <journey-id>");
1697
+ console.error("Usage: ascendkit journey pause <journey-id> [--yes]");
1513
1698
  return await exitCli(1);
1514
1699
  }
1700
+ const journeyForPause = await journeys.getJourney(client, rest[0]);
1701
+ const activeUsers = journeyForPause?.stats?.currentlyActive ?? 0;
1702
+ if (activeUsers > 0 && !flags.yes) {
1703
+ if (process.stdin.isTTY && process.stdout.isTTY) {
1704
+ const { createInterface } = await import("node:readline/promises");
1705
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1706
+ try {
1707
+ const answer = (await rl.question(`${activeUsers} user(s) are currently active in this journey. Actions will be queued while paused. Proceed? [y/N] `)).trim().toLowerCase();
1708
+ if (answer !== "y" && answer !== "yes") {
1709
+ console.log("Cancelled.");
1710
+ return;
1711
+ }
1712
+ }
1713
+ finally {
1714
+ rl.close();
1715
+ }
1716
+ }
1717
+ else {
1718
+ console.error(`Warning: ${activeUsers} user(s) are currently active. Pass --yes to skip confirmation.`);
1719
+ return await exitCli(1);
1720
+ }
1721
+ }
1515
1722
  console.log(formatJourneyWithGuidance(await journeys.pauseJourney(client, rest[0])));
1516
1723
  break;
1724
+ }
1517
1725
  case "resume":
1518
1726
  if (!rest[0]) {
1519
1727
  console.error("Usage: ascendkit journey resume <journey-id>");
@@ -1559,7 +1767,7 @@ async function runJourney(client, action, rest) {
1559
1767
  }
1560
1768
  if (flags.terminal)
1561
1769
  params.terminal = flags.terminal === "true";
1562
- console.log(formatSingleNode(await journeys.addNode(client, rest[0], params), "Added", flags.name));
1770
+ console.log(formatSingleNode(await journeys.addNode(client, rest[0], params), "Added", flags.name, { suppressWarnings: !!flags.quiet }));
1563
1771
  break;
1564
1772
  }
1565
1773
  case "edit-node": {
@@ -1634,7 +1842,7 @@ async function runJourney(client, action, rest) {
1634
1842
  if (flags.name)
1635
1843
  params.name = flags.name;
1636
1844
  const transitionName = flags.name || `${flags.from}-to-${flags.to}`;
1637
- console.log(formatSingleTransition(await journeys.addTransition(client, rest[0], params), "Added", transitionName));
1845
+ console.log(formatSingleTransition(await journeys.addTransition(client, rest[0], params), "Added", transitionName, { suppressWarnings: !!flags.quiet }));
1638
1846
  break;
1639
1847
  }
1640
1848
  case "edit-transition": {
@@ -1677,10 +1885,10 @@ async function runWebhook(client, action, rest) {
1677
1885
  console.error("Usage: ascendkit webhook create --url <url> [--events <e1,e2,...>]");
1678
1886
  return await exitCli(1);
1679
1887
  }
1680
- output(await webhooks.createWebhook(client, {
1888
+ printWebhookSummary(await webhooks.createWebhook(client, {
1681
1889
  url: flags.url,
1682
1890
  events: flags.events ? flags.events.split(",") : undefined,
1683
- }));
1891
+ }), { showSecret: true });
1684
1892
  break;
1685
1893
  case "list":
1686
1894
  table(await webhooks.listWebhooks(client), [
@@ -1695,7 +1903,7 @@ async function runWebhook(client, action, rest) {
1695
1903
  console.error("Usage: ascendkit webhook get <webhook-id>");
1696
1904
  return await exitCli(1);
1697
1905
  }
1698
- output(await webhooks.getWebhook(client, rest[0]));
1906
+ printWebhookSummary(await webhooks.getWebhook(client, rest[0]));
1699
1907
  break;
1700
1908
  case "update": {
1701
1909
  if (!rest[0]) {
@@ -1718,14 +1926,15 @@ async function runWebhook(client, action, rest) {
1718
1926
  console.error("Usage: ascendkit webhook delete <webhook-id>");
1719
1927
  return await exitCli(1);
1720
1928
  }
1721
- output(await webhooks.deleteWebhook(client, rest[0]));
1929
+ await webhooks.deleteWebhook(client, rest[0]);
1930
+ console.log("Webhook deleted.");
1722
1931
  break;
1723
1932
  case "test":
1724
1933
  if (!rest[0]) {
1725
1934
  console.error("Usage: ascendkit webhook test <webhook-id> [--event <event-type>]");
1726
1935
  return await exitCli(1);
1727
1936
  }
1728
- output(await webhooks.testWebhook(client, rest[0], flags.event));
1937
+ printWebhookTestResult(await webhooks.testWebhook(client, rest[0], flags.event));
1729
1938
  break;
1730
1939
  default:
1731
1940
  console.error(`Unknown webhook command: ${action}`);
@@ -1753,7 +1962,7 @@ async function runCampaign(client, action, rest) {
1753
1962
  console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1754
1963
  return await exitCli(1);
1755
1964
  }
1756
- output(await campaigns.createCampaign(client, {
1965
+ printCampaignSummary(await campaigns.createCampaign(client, {
1757
1966
  name: flags.name,
1758
1967
  templateId: flags.template,
1759
1968
  audienceFilter: createFilter,
@@ -1778,7 +1987,7 @@ async function runCampaign(client, action, rest) {
1778
1987
  console.error("Usage: ascendkit campaign show <campaign-id>");
1779
1988
  return await exitCli(1);
1780
1989
  }
1781
- output(await campaigns.getCampaign(client, rest[0]));
1990
+ printCampaignSummary(await campaigns.getCampaign(client, rest[0]));
1782
1991
  break;
1783
1992
  case "update": {
1784
1993
  if (!rest[0]) {
@@ -1795,7 +2004,7 @@ async function runCampaign(client, action, rest) {
1795
2004
  return await exitCli(1);
1796
2005
  }
1797
2006
  }
1798
- output(await campaigns.updateCampaign(client, rest[0], {
2007
+ printCampaignSummary(await campaigns.updateCampaign(client, rest[0], {
1799
2008
  name: flags.name,
1800
2009
  templateId: flags.template,
1801
2010
  audienceFilter: updateFilter,
@@ -1813,7 +2022,7 @@ async function runCampaign(client, action, rest) {
1813
2022
  console.error("Campaign has no audience filter set.");
1814
2023
  return await exitCli(1);
1815
2024
  }
1816
- output(await campaigns.previewAudience(client, detail.audienceFilter));
2025
+ printAudiencePreview(await campaigns.previewAudience(client, detail.audienceFilter));
1817
2026
  break;
1818
2027
  }
1819
2028
  case "schedule":
@@ -1821,21 +2030,22 @@ async function runCampaign(client, action, rest) {
1821
2030
  console.error("Usage: ascendkit campaign schedule <campaign-id> --at <datetime>");
1822
2031
  return await exitCli(1);
1823
2032
  }
1824
- output(await campaigns.updateCampaign(client, rest[0], { scheduledAt: flags.at }));
2033
+ printCampaignSummary(await campaigns.updateCampaign(client, rest[0], { scheduledAt: flags.at }));
1825
2034
  break;
1826
2035
  case "cancel":
1827
2036
  if (!rest[0]) {
1828
2037
  console.error("Usage: ascendkit campaign cancel <campaign-id>");
1829
2038
  return await exitCli(1);
1830
2039
  }
1831
- output(await campaigns.deleteCampaign(client, rest[0]));
2040
+ await campaigns.deleteCampaign(client, rest[0]);
2041
+ console.log("Campaign cancelled.");
1832
2042
  break;
1833
2043
  case "analytics":
1834
2044
  if (!rest[0]) {
1835
2045
  console.error("Usage: ascendkit campaign analytics <campaign-id>");
1836
2046
  return await exitCli(1);
1837
2047
  }
1838
- output(await campaigns.getCampaignAnalytics(client, rest[0]));
2048
+ printCampaignAnalytics(await campaigns.getCampaignAnalytics(client, rest[0]));
1839
2049
  break;
1840
2050
  default:
1841
2051
  console.error(`Unknown campaign command: ${action}`);
@@ -1895,7 +2105,8 @@ async function runEmail(client, action, rest) {
1895
2105
  break;
1896
2106
  }
1897
2107
  case "remove-domain":
1898
- output(await email.removeDomain(client));
2108
+ await email.removeDomain(client);
2109
+ console.log("Domain removed.");
1899
2110
  break;
1900
2111
  case "list": {
1901
2112
  const result = await email.listIdentities(client);
@@ -1908,10 +2119,25 @@ async function runEmail(client, action, rest) {
1908
2119
  console.error("Usage: ascendkit email-identity add <email> [--display-name <name>]");
1909
2120
  return await exitCli(1);
1910
2121
  }
1911
- output(await email.createIdentity(client, {
2122
+ const added = await email.createIdentity(client, {
1912
2123
  email: identityEmail,
1913
2124
  displayName: flags["display-name"],
1914
- }));
2125
+ });
2126
+ printEmailIdentities(added.identities ?? []);
2127
+ console.log("Verification email sent.");
2128
+ break;
2129
+ }
2130
+ case "update": {
2131
+ const identityEmail = rest[0];
2132
+ if (!identityEmail || !flags["display-name"]) {
2133
+ console.error("Usage: ascendkit email-identity update <email> --display-name <name>");
2134
+ return await exitCli(1);
2135
+ }
2136
+ const updated = await email.updateIdentity(client, {
2137
+ email: identityEmail,
2138
+ displayName: flags["display-name"],
2139
+ });
2140
+ printEmailIdentities(updated.identities ?? []);
1915
2141
  break;
1916
2142
  }
1917
2143
  case "resend": {
@@ -1920,7 +2146,8 @@ async function runEmail(client, action, rest) {
1920
2146
  console.error("Usage: ascendkit email-identity resend <email>");
1921
2147
  return await exitCli(1);
1922
2148
  }
1923
- output(await email.resendIdentityVerification(client, identityEmail));
2149
+ await email.resendIdentityVerification(client, identityEmail);
2150
+ console.log(`Verification email sent to ${identityEmail}.`);
1924
2151
  break;
1925
2152
  }
1926
2153
  case "set-default": {
@@ -1929,10 +2156,11 @@ async function runEmail(client, action, rest) {
1929
2156
  console.error("Usage: ascendkit email-identity set-default <email> [--display-name <name>]");
1930
2157
  return await exitCli(1);
1931
2158
  }
1932
- output(await email.setDefaultIdentity(client, {
2159
+ const updated = await email.setDefaultIdentity(client, {
1933
2160
  email: identityEmail,
1934
2161
  displayName: flags["display-name"],
1935
- }));
2162
+ });
2163
+ printEmailIdentities(updated.identities ?? []);
1936
2164
  break;
1937
2165
  }
1938
2166
  case "remove":
@@ -1942,7 +2170,9 @@ async function runEmail(client, action, rest) {
1942
2170
  console.error("Usage: ascendkit email-identity remove <email>");
1943
2171
  return await exitCli(1);
1944
2172
  }
1945
- output(await email.removeIdentity(client, identityEmail));
2173
+ const remaining = await email.removeIdentity(client, identityEmail);
2174
+ console.log("Identity removed.");
2175
+ printEmailIdentities(remaining.identities ?? []);
1946
2176
  break;
1947
2177
  }
1948
2178
  case "test": {
@@ -1951,10 +2181,11 @@ async function runEmail(client, action, rest) {
1951
2181
  console.error("Usage: ascendkit email-identity test <email> --to <recipient>");
1952
2182
  return await exitCli(1);
1953
2183
  }
1954
- output(await email.sendTestEmail(client, {
2184
+ const testResult = await email.sendTestEmail(client, {
1955
2185
  to: flags.to,
1956
2186
  fromIdentityEmail: identityEmail,
1957
- }));
2187
+ });
2188
+ console.log(testResult.message ?? "Test email sent.");
1958
2189
  break;
1959
2190
  }
1960
2191
  default:
@@ -1967,12 +2198,12 @@ run().catch((err) => {
1967
2198
  exitCli(1, err instanceof Error ? err : new Error(String(err)));
1968
2199
  });
1969
2200
  async function printEmailSetup(settings) {
1970
- output(settings);
1971
2201
  if (!settings.domain)
1972
2202
  return;
1973
2203
  const provider = settings.dnsProvider;
2204
+ console.log(`Domain: ${settings.domain}`);
1974
2205
  if (provider?.name) {
1975
- console.log(`\nDetected DNS provider: ${provider.name} (${provider.confidence ?? "unknown"})`);
2206
+ console.log(`DNS provider: ${provider.name} (${provider.confidence ?? "unknown"})`);
1976
2207
  }
1977
2208
  if (provider?.portalUrl) {
1978
2209
  console.log(`Provider console: ${provider.portalUrl}`);
@@ -1980,10 +2211,54 @@ async function printEmailSetup(settings) {
1980
2211
  if (provider?.assistantSetupUrl) {
1981
2212
  console.log(`Guided setup: ${provider.assistantSetupUrl}`);
1982
2213
  }
1983
- if (Array.isArray(settings.dnsRecords) && settings.dnsRecords.length > 0) {
1984
- console.log("\nAdd these DNS records:");
1985
- for (const rec of settings.dnsRecords) {
1986
- console.log(` ${rec.type}\t${rec.name}\t${rec.value}`);
2214
+ const records = Array.isArray(settings.dnsRecords) ? settings.dnsRecords : [];
2215
+ if (records.length > 0) {
2216
+ console.log("\nDNS records to add:");
2217
+ for (const rec of records) {
2218
+ console.log(` ${rec.type.padEnd(6)} ${rec.name} ${rec.value}`);
2219
+ }
2220
+ if (process.stdin.isTTY && process.stdout.isTTY) {
2221
+ const defaultFile = `dns-records-${settings.domain}.txt`;
2222
+ const { createInterface } = await import("node:readline/promises");
2223
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2224
+ try {
2225
+ const answer = (await rl.question(`\nSave records to file [${defaultFile}]: `)).trim();
2226
+ const filename = answer || defaultFile;
2227
+ const { existsSync, writeFileSync } = await import("node:fs");
2228
+ if (existsSync(filename)) {
2229
+ const overwrite = (await rl.question(`${filename} already exists. Overwrite? [y/N] `)).trim().toLowerCase();
2230
+ if (overwrite !== "y" && overwrite !== "yes") {
2231
+ console.log("Skipped.");
2232
+ return;
2233
+ }
2234
+ }
2235
+ const lines = [
2236
+ `; DNS records for ${settings.domain}`,
2237
+ `; Generated by AscendKit CLI`,
2238
+ "",
2239
+ ...records.map(rec => {
2240
+ let value;
2241
+ if (rec.type === "TXT") {
2242
+ value = `"${rec.value}"`;
2243
+ }
2244
+ else if (rec.type === "CNAME" || rec.type === "MX") {
2245
+ // Ensure absolute FQDN with trailing dot to prevent zone-relative expansion
2246
+ const target = rec.type === "MX" ? rec.value.replace(/^(\d+)\s+/, "$1 ") : rec.value;
2247
+ value = target.endsWith(".") ? target : `${target}.`;
2248
+ }
2249
+ else {
2250
+ value = rec.value;
2251
+ }
2252
+ const name = rec.name.endsWith(".") ? rec.name : `${rec.name}.`;
2253
+ return `${name}\t3600\tIN\t${rec.type}\t${value} ; cf_tags=cf-proxied:false`;
2254
+ }),
2255
+ ];
2256
+ writeFileSync(filename, lines.join("\n") + "\n");
2257
+ console.log(`Saved to ${filename}`);
2258
+ }
2259
+ finally {
2260
+ rl.close();
2261
+ }
1987
2262
  }
1988
2263
  }
1989
2264
  }
@@ -2015,8 +2290,20 @@ function printEmailStatusSummary(settings, domainStatus, dnsCheck, identities) {
2015
2290
  return;
2016
2291
  }
2017
2292
  console.log(`Domain: ${settings.domain} (${domainStatus.status || settings.verificationStatus || "unknown"})`);
2018
- if (dnsCheck?.summary) {
2019
- console.log(`DNS: ${dnsCheck.summary.found}/${dnsCheck.summary.total} verified`);
2293
+ if (dnsCheck) {
2294
+ const { summary, records } = dnsCheck;
2295
+ if (summary.notFound === 0 && summary.mismatch === 0 && summary.errored === 0) {
2296
+ console.log("DNS: all records verified");
2297
+ }
2298
+ else {
2299
+ console.log(`DNS: ${summary.found}/${summary.total} verified`);
2300
+ const pending = records.filter(r => !r.found || r.mismatch);
2301
+ if (pending.length > 0) {
2302
+ console.log("Pending:");
2303
+ for (const r of pending)
2304
+ console.log(` ${r.type.padEnd(6)} ${r.name}`);
2305
+ }
2306
+ }
2020
2307
  }
2021
2308
  console.log("");
2022
2309
  printEmailIdentities(identities);
@@ -6,12 +6,14 @@ export interface CreateTemplateParams {
6
6
  bodyText: string;
7
7
  slug?: string;
8
8
  description?: string;
9
+ category?: "marketing" | "transactional";
9
10
  }
10
11
  export interface UpdateTemplateParams {
11
12
  subject?: string;
12
13
  bodyHtml?: string;
13
14
  bodyText?: string;
14
15
  changeNote?: string;
16
+ category?: "marketing" | "transactional";
15
17
  }
16
18
  export declare function createTemplate(client: AscendKitClient, params: CreateTemplateParams): Promise<unknown>;
17
19
  export declare function listTemplates(client: AscendKitClient, params?: {
@@ -80,6 +80,10 @@ export declare function createIdentity(client: AscendKitClient, identity: {
80
80
  displayName?: string;
81
81
  }): Promise<EmailIdentitiesResponse>;
82
82
  export declare function resendIdentityVerification(client: AscendKitClient, email: string): Promise<EmailIdentitiesResponse>;
83
+ export declare function updateIdentity(client: AscendKitClient, identity: {
84
+ email: string;
85
+ displayName: string;
86
+ }): Promise<EmailIdentitiesResponse>;
83
87
  export declare function setDefaultIdentity(client: AscendKitClient, identity: {
84
88
  email: string;
85
89
  displayName?: string;
@@ -37,6 +37,9 @@ export async function createIdentity(client, identity) {
37
37
  export async function resendIdentityVerification(client, email) {
38
38
  return client.managedPost(`/api/email/settings/identities/${encodeURIComponent(email)}/resend`, {});
39
39
  }
40
+ export async function updateIdentity(client, identity) {
41
+ return client.managedPost(`/api/email/settings/identities/${encodeURIComponent(identity.email)}/update`, { displayName: identity.displayName });
42
+ }
40
43
  export async function setDefaultIdentity(client, identity) {
41
44
  return client.managedPost(`/api/email/settings/identities/${encodeURIComponent(identity.email)}/default`, { displayName: identity.displayName ?? "" });
42
45
  }
@@ -64,6 +64,13 @@ export function registerEmailTools(server, client) {
64
64
  const data = await email.resendIdentityVerification(client, params.email);
65
65
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
66
66
  });
67
+ server.tool("email_identity_update", "Update the display name of a sender identity", {
68
+ email: z.string().describe("Sender identity email"),
69
+ displayName: z.string().describe("New display name"),
70
+ }, async (params) => {
71
+ const data = await email.updateIdentity(client, params);
72
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
73
+ });
67
74
  server.tool("email_identity_set_default", "Set the default sender identity for this environment", {
68
75
  email: z.string().describe("Sender identity email"),
69
76
  displayName: z.string().optional().describe("Optional updated display name"),
@@ -22,6 +22,7 @@ interface JourneyData {
22
22
  terminal?: boolean;
23
23
  }>;
24
24
  transitions?: Array<{
25
+ name?: string;
25
26
  from?: string;
26
27
  from_?: string;
27
28
  to?: string;
@@ -75,7 +76,9 @@ interface AnalyticsData {
75
76
  exited?: number;
76
77
  };
77
78
  }
78
- export declare function formatJourneyWithGuidance(journey: JourneyData): string;
79
+ export declare function formatJourneyWithGuidance(journey: JourneyData, opts?: {
80
+ suppressWarnings?: boolean;
81
+ }): string;
79
82
  export declare function formatJourneyList(data: {
80
83
  journeys: JourneyData[];
81
84
  total: number;
@@ -109,10 +112,14 @@ interface TransitionListItem {
109
112
  export declare function formatNodeList(data: {
110
113
  nodes: NodeListItem[];
111
114
  }): string;
112
- export declare function formatSingleNode(data: JourneyData, action: string, nodeName: string): string;
115
+ export declare function formatSingleNode(data: JourneyData, action: string, nodeName: string, opts?: {
116
+ suppressWarnings?: boolean;
117
+ }): string;
113
118
  export declare function formatTransitionList(data: {
114
119
  transitions: TransitionListItem[];
115
120
  }): string;
116
- export declare function formatSingleTransition(data: JourneyData, action: string, transitionName: string): string;
121
+ export declare function formatSingleTransition(data: JourneyData, action: string, transitionName: string, opts?: {
122
+ suppressWarnings?: boolean;
123
+ }): string;
117
124
  export declare function formatJourneyAnalytics(data: AnalyticsData): string;
118
125
  export {};
@@ -14,7 +14,7 @@ function nodeCount(journey) {
14
14
  function transitionCount(journey) {
15
15
  return (journey.transitions || []).length;
16
16
  }
17
- export function formatJourneyWithGuidance(journey) {
17
+ export function formatJourneyWithGuidance(journey, opts) {
18
18
  const nodes = nodeCount(journey);
19
19
  const transitions = transitionCount(journey);
20
20
  const stats = journey.stats;
@@ -50,11 +50,16 @@ export function formatJourneyWithGuidance(journey) {
50
50
  else {
51
51
  desc = trigger?.event || "event";
52
52
  }
53
- lines.push(` ${from} ${t.to} [${desc}]`);
53
+ const id = t.name ? ` (${t.name})` : "";
54
+ lines.push(` ${from} → ${t.to} [${desc}]${id}`);
54
55
  }
55
56
  }
56
- // Warnings
57
- if (journey.warnings && journey.warnings.length > 0) {
57
+ // Warnings — suppressed on paused journeys (mid-construction) or when opts.suppressWarnings is set
58
+ const showWarnings = !opts?.suppressWarnings &&
59
+ journey.status !== "paused" &&
60
+ journey.warnings &&
61
+ journey.warnings.length > 0;
62
+ if (showWarnings) {
58
63
  lines.push("");
59
64
  lines.push("Warnings:");
60
65
  for (const w of journey.warnings) {
@@ -146,8 +151,8 @@ export function formatNodeList(data) {
146
151
  }
147
152
  return lines.join("\n");
148
153
  }
149
- export function formatSingleNode(data, action, nodeName) {
150
- return `${action} node '${nodeName}'.\n\n${formatJourneyWithGuidance(data)}`;
154
+ export function formatSingleNode(data, action, nodeName, opts) {
155
+ return `${action} node '${nodeName}'.\n\n${formatJourneyWithGuidance(data, opts)}`;
151
156
  }
152
157
  export function formatTransitionList(data) {
153
158
  const transitions = data.transitions || [];
@@ -162,8 +167,8 @@ export function formatTransitionList(data) {
162
167
  }
163
168
  return lines.join("\n");
164
169
  }
165
- export function formatSingleTransition(data, action, transitionName) {
166
- return `${action} transition '${transitionName}'.\n\n${formatJourneyWithGuidance(data)}`;
170
+ export function formatSingleTransition(data, action, transitionName, opts) {
171
+ return `${action} transition '${transitionName}'.\n\n${formatJourneyWithGuidance(data, opts)}`;
167
172
  }
168
173
  export function formatJourneyAnalytics(data) {
169
174
  const { journey, nodes, transitions, terminalDistribution, totals } = data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascendkit/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.8",
4
4
  "description": "AscendKit CLI and MCP server",
5
5
  "author": "ascendkit.dev",
6
6
  "license": "MIT",