@ascendkit/cli 0.3.0 → 0.3.2

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
@@ -14,6 +14,11 @@ import * as webhooks from "./commands/webhooks.js";
14
14
  import * as campaigns from "./commands/campaigns.js";
15
15
  import * as importCmd from "./commands/import.js";
16
16
  import { parseDelay } from "./utils/duration.js";
17
+ import { exitCli, installGlobalHandlers, onExit } from "./utils/exit.js";
18
+ import { getInvocationId, getMachineId } from "./utils/correlation.js";
19
+ import { redactArgs } from "./utils/redaction.js";
20
+ import { captureTelemetry } from "./utils/telemetry.js";
21
+ import { hostname as osHostname, platform as osPlatform } from "node:os";
17
22
  import { formatJourneyAnalytics, formatJourneyWithGuidance, formatNodeList, formatSingleNode, formatSingleTransition, formatTransitionList, } from "./utils/journey-format.js";
18
23
  const require = createRequire(import.meta.url);
19
24
  const { version: CLI_VERSION } = require("../package.json");
@@ -217,7 +222,8 @@ function getClient() {
217
222
  // Require auth unless env var provides the public key (CI/CD escape hatch)
218
223
  if (!auth?.token && !publicKey) {
219
224
  console.error("Not initialized. Run: ascendkit init");
220
- process.exit(1);
225
+ exitCli(1);
226
+ throw new Error("unreachable");
221
227
  }
222
228
  if (!publicKey && env?.publicKey) {
223
229
  publicKey = env.publicKey;
@@ -227,7 +233,8 @@ function getClient() {
227
233
  }
228
234
  if (!publicKey) {
229
235
  console.error("No environment set. Run: ascendkit set-env <public-key>");
230
- process.exit(1);
236
+ exitCli(1);
237
+ throw new Error("unreachable");
231
238
  }
232
239
  const client = new AscendKitClient({
233
240
  apiUrl: apiUrl ?? DEFAULT_API_URL,
@@ -280,7 +287,7 @@ function printAuthSettingsSummary(data) {
280
287
  console.log(`Session: ${data.sessionDuration}`);
281
288
  }
282
289
  }
283
- function printTemplateSummary(data) {
290
+ function printTemplateSummary(data, opts) {
284
291
  console.log(`Template: ${data.name} (${data.id})`);
285
292
  if (data.slug)
286
293
  console.log(`Slug: ${data.slug}`);
@@ -290,6 +297,35 @@ function printTemplateSummary(data) {
290
297
  if (Array.isArray(data.variables) && data.variables.length > 0) {
291
298
  console.log(`Variables: ${data.variables.join(", ")}`);
292
299
  }
300
+ if (Array.isArray(data.unconfiguredVariables) && data.unconfiguredVariables.length > 0) {
301
+ console.log(`\n⚠ Unconfigured variables: ${data.unconfiguredVariables.join(", ")}`);
302
+ console.log(` Set values with: ascendkit keystore set <key> <value>`);
303
+ }
304
+ if (Array.isArray(data.relatedJourneys) && data.relatedJourneys.length > 0) {
305
+ const refs = data.relatedJourneys.map((j) => `${j.name} (${j.id})`).join(", ");
306
+ console.log(`Used in journeys: ${refs}`);
307
+ }
308
+ // Show body content on show/get (verbose) but not on create/update summaries
309
+ if (opts?.verbose) {
310
+ if (data.bodyHtml) {
311
+ console.log(`\n--- HTML Body ---\n${data.bodyHtml}`);
312
+ }
313
+ if (data.bodyText) {
314
+ console.log(`\n--- Plain Text Body ---\n${data.bodyText}`);
315
+ }
316
+ }
317
+ // Always show warnings and variable diffs (relevant on update responses)
318
+ if (Array.isArray(data.droppedVariables) && data.droppedVariables.length > 0) {
319
+ console.log(`\n⚠ Variables removed: ${data.droppedVariables.join(", ")}`);
320
+ }
321
+ if (Array.isArray(data.addedVariables) && data.addedVariables.length > 0) {
322
+ console.log(`Variables added: ${data.addedVariables.join(", ")}`);
323
+ }
324
+ if (Array.isArray(data.warnings) && data.warnings.length > 0) {
325
+ for (const w of data.warnings) {
326
+ console.log(`⚠ ${w}`);
327
+ }
328
+ }
293
329
  }
294
330
  function printSurveySummary(data) {
295
331
  console.log(`Survey: ${data.name} (${data.id})`);
@@ -302,6 +338,65 @@ function printSurveySummary(data) {
302
338
  if (questions != null)
303
339
  console.log(`Questions: ${questions}`);
304
340
  }
341
+ function printEnvironmentSummary(data) {
342
+ console.log(`${data.name} (${data.id})`);
343
+ console.log(`Tier: ${data.tier}`);
344
+ if (data.description)
345
+ console.log(`Description: ${data.description}`);
346
+ console.log(`Public key: ${data.publicKey}`);
347
+ if (data.secretKey) {
348
+ const masked = data.secretKey.slice(0, 10) + "****";
349
+ console.log(`Secret key: ${masked}`);
350
+ }
351
+ const vars = data.variables && typeof data.variables === "object" ? Object.entries(data.variables) : [];
352
+ if (vars.length > 0) {
353
+ console.log(`Variables:`);
354
+ for (const [k, v] of vars)
355
+ console.log(` ${k}=${v}`);
356
+ }
357
+ }
358
+ function printPromotionSummary(result) {
359
+ const src = result.source ?? {};
360
+ const tgt = result.target ?? {};
361
+ console.log(`Promoted ${src.tier} → ${tgt.tier} (${tgt.envId})`);
362
+ console.log("");
363
+ const changes = result.changes ?? {};
364
+ const rows = [];
365
+ for (const [key, val] of Object.entries(changes)) {
366
+ const label = key.padEnd(14);
367
+ if (val.action === "unchanged") {
368
+ rows.push([label, "unchanged"]);
369
+ }
370
+ else if (val.action === "skip") {
371
+ rows.push([label, `skipped — ${val.reason}`]);
372
+ }
373
+ else if (val.action === "update" || val.action === "add_only") {
374
+ const fields = Array.isArray(val.fields) && val.fields.length ? ` (${val.fields.join(", ")})` : "";
375
+ rows.push([label, `updated${fields}`]);
376
+ }
377
+ else if (val.items) {
378
+ const counts = Object.entries(val.counts ?? {}).map(([k, v]) => `${v} ${k}`).join(", ");
379
+ rows.push([label, counts || "unchanged"]);
380
+ }
381
+ else {
382
+ rows.push([label, val.action ?? "—"]);
383
+ }
384
+ }
385
+ for (const [label, status] of rows) {
386
+ console.log(` ${label} ${status}`);
387
+ }
388
+ // Only show warnings not already conveyed by a skipped row in the table
389
+ const skippedKeys = new Set(Object.entries(changes)
390
+ .filter(([, v]) => v.action === "skip")
391
+ .map(([k]) => k.toUpperCase()));
392
+ const warnings = (Array.isArray(result.warnings) ? result.warnings : [])
393
+ .filter((w) => !skippedKeys.size || !Array.from(skippedKeys).some(k => w.code?.toUpperCase().includes(k)));
394
+ if (warnings.length > 0) {
395
+ console.log("");
396
+ for (const w of warnings)
397
+ console.log(`⚠ ${w.message}`);
398
+ }
399
+ }
305
400
  function printProjectSummary(data) {
306
401
  console.log(`Project: ${data.id}`);
307
402
  console.log(`Name: ${data.name}`);
@@ -348,7 +443,36 @@ function table(rows, columns) {
348
443
  }
349
444
  }
350
445
  async function run() {
446
+ installGlobalHandlers();
351
447
  const args = process.argv.slice(2);
448
+ const dtEntered = new Date();
449
+ // Extract command/domain/action for telemetry before any early returns
450
+ const teleDomain = args[0] ?? null;
451
+ const teleAction = args[1] ?? null;
452
+ const redactedArgs = redactArgs(args);
453
+ // Register telemetry hook — runs on every exit path (success or failure)
454
+ onExit(async (code, error) => {
455
+ const dtCompleted = new Date();
456
+ const record = {
457
+ invocationId: getInvocationId(),
458
+ machineId: getMachineId(),
459
+ clientType: "cli",
460
+ clientVersion: CLI_VERSION,
461
+ command: redactedArgs.join(" ") || "(empty)",
462
+ domain: teleDomain,
463
+ action: teleAction,
464
+ args: redactedArgs,
465
+ dtEntered: dtEntered.toISOString(),
466
+ dtCompleted: dtCompleted.toISOString(),
467
+ durationMs: dtCompleted.getTime() - dtEntered.getTime(),
468
+ success: code === 0,
469
+ errorMessage: error?.message ?? null,
470
+ hostname: osHostname(),
471
+ os: osPlatform(),
472
+ nodeVersion: process.version,
473
+ };
474
+ await captureTelemetry(record);
475
+ });
352
476
  if (args[0] === "--version" || args[0] === "-v" || args[0] === "-V") {
353
477
  console.log(CLI_VERSION);
354
478
  return;
@@ -362,7 +486,7 @@ async function run() {
362
486
  if (!printSectionHelp(args[1])) {
363
487
  console.error(`Unknown help section: ${args[1]}`);
364
488
  console.error('Run "ascendkit --help" for available sections.');
365
- process.exit(1);
489
+ return await exitCli(1);
366
490
  }
367
491
  return;
368
492
  }
@@ -378,7 +502,7 @@ async function run() {
378
502
  if (!printSectionHelp(domain)) {
379
503
  console.error(`Unknown command section: ${domain}`);
380
504
  console.error('Run "ascendkit --help" for usage');
381
- process.exit(1);
505
+ return await exitCli(1);
382
506
  }
383
507
  return;
384
508
  }
@@ -404,7 +528,7 @@ async function run() {
404
528
  else if (action === "show") {
405
529
  if (!args[2]) {
406
530
  console.error("Usage: ascendkit project show <project-id>");
407
- process.exit(1);
531
+ return await exitCli(1);
408
532
  }
409
533
  printProjectSummary(await platform.showProject(args[2]));
410
534
  }
@@ -415,7 +539,7 @@ async function run() {
415
539
  const flags = parseFlags(args.slice(2));
416
540
  if (!flags.name) {
417
541
  console.error("Usage: ascendkit project create --name <name> [--description <description>] [--services <s1,s2,...>]");
418
- process.exit(1);
542
+ return await exitCli(1);
419
543
  }
420
544
  try {
421
545
  const proj = await platform.createProject(flags.name, flags.description, flags.services?.split(","));
@@ -435,18 +559,18 @@ async function run() {
435
559
  catch { /* use raw message */ }
436
560
  }
437
561
  console.error(message);
438
- process.exit(1);
562
+ return await exitCli(1);
439
563
  }
440
564
  }
441
565
  else {
442
566
  console.error('Usage: ascendkit project list|create|show|env');
443
- process.exit(1);
567
+ return await exitCli(1);
444
568
  }
445
569
  return;
446
570
  case "set-env":
447
571
  if (!action) {
448
572
  console.error("Usage: ascendkit set-env <public-key>");
449
- process.exit(1);
573
+ return await exitCli(1);
450
574
  }
451
575
  await platform.setEnv(action);
452
576
  return;
@@ -495,7 +619,7 @@ async function run() {
495
619
  default:
496
620
  console.error(`Unknown command: ${domain}`);
497
621
  console.error('Run "ascendkit --help" for usage');
498
- process.exit(1);
622
+ return await exitCli(1);
499
623
  }
500
624
  }
501
625
  async function runImport(client, source, rest) {
@@ -529,7 +653,7 @@ async function runImport(client, source, rest) {
529
653
  console.error(`Unsupported import source: ${source}`);
530
654
  console.error("Supported sources: clerk");
531
655
  console.error('Run "ascendkit help import" for usage');
532
- process.exit(1);
656
+ return await exitCli(1);
533
657
  }
534
658
  const dryRun = action !== "run";
535
659
  const hasUsers = flags.users !== undefined;
@@ -542,7 +666,7 @@ async function runImport(client, source, rest) {
542
666
  const filePath = flags.file;
543
667
  if (!apiKey && !filePath) {
544
668
  console.error("Usage: ascendkit import clerk preview|run --api-key <key> | --file <path>");
545
- process.exit(1);
669
+ return await exitCli(1);
546
670
  }
547
671
  let clerkUsers = [];
548
672
  if (filePath) {
@@ -668,7 +792,7 @@ async function runProjectEnvironment(rest) {
668
792
  case "list":
669
793
  if (!target) {
670
794
  console.error("Usage: ascendkit project env list <project-id>");
671
- process.exit(1);
795
+ return await exitCli(1);
672
796
  }
673
797
  table(await platform.listEnvironments(target), [
674
798
  { key: "id", label: "ID" },
@@ -679,7 +803,7 @@ async function runProjectEnvironment(rest) {
679
803
  return;
680
804
  default:
681
805
  console.error("Usage: ascendkit project env list <project-id>");
682
- process.exit(1);
806
+ return await exitCli(1);
683
807
  }
684
808
  }
685
809
  async function runEnvironment(action, rest) {
@@ -687,23 +811,22 @@ async function runEnvironment(action, rest) {
687
811
  const ctx = loadEnvContext();
688
812
  if (!ctx) {
689
813
  console.error("No environment set. Run: ascendkit set-env <public-key>");
690
- process.exit(1);
814
+ return await exitCli(1);
691
815
  }
692
816
  switch (action) {
693
817
  case "show":
694
- output(await platform.getEnvironment(ctx.projectId, ctx.environmentId));
818
+ printEnvironmentSummary(await platform.getEnvironment(ctx.projectId, ctx.environmentId));
695
819
  return;
696
820
  case "promote": {
697
821
  const envId = rest[0] && !rest[0].startsWith("--") ? rest[0] : ctx.environmentId;
698
822
  const target = flags.target;
699
823
  if (!envId || !target) {
700
824
  console.error("Usage: ascendkit environment promote [<env-id>] --target <tier>");
701
- process.exit(1);
825
+ return await exitCli(1);
702
826
  }
703
827
  try {
704
828
  const result = await platform.promoteEnvironment(envId, target);
705
- console.log("Promotion successful:");
706
- console.log(JSON.stringify(result, null, 2));
829
+ printPromotionSummary(result);
707
830
  }
708
831
  catch (err) {
709
832
  let message = err instanceof Error ? err.message : String(err);
@@ -719,7 +842,7 @@ async function runEnvironment(action, rest) {
719
842
  catch { /* use raw message */ }
720
843
  }
721
844
  console.error(message);
722
- process.exit(1);
845
+ return await exitCli(1);
723
846
  }
724
847
  return;
725
848
  }
@@ -729,7 +852,7 @@ async function runEnvironment(action, rest) {
729
852
  const description = flags.description;
730
853
  if (!name && description === undefined) {
731
854
  console.error("Provide at least --name or --description to update.");
732
- process.exit(1);
855
+ return await exitCli(1);
733
856
  }
734
857
  try {
735
858
  const result = await platform.updateEnvironment(ctx.projectId, envId, name, description);
@@ -750,21 +873,21 @@ async function runEnvironment(action, rest) {
750
873
  catch { /* use raw message */ }
751
874
  }
752
875
  console.error(message);
753
- process.exit(1);
876
+ return await exitCli(1);
754
877
  }
755
878
  return;
756
879
  }
757
880
  default:
758
881
  console.error(`Unknown environment command: ${action}`);
759
882
  console.error("Usage: ascendkit environment show|update|promote");
760
- process.exit(1);
883
+ return await exitCli(1);
761
884
  }
762
885
  }
763
886
  async function runKeystore(action, rest) {
764
887
  const ctx = loadEnvContext();
765
888
  if (!ctx) {
766
889
  console.error("No environment set. Run: ascendkit set-env <public-key>");
767
- process.exit(1);
890
+ return await exitCli(1);
768
891
  }
769
892
  const current = await platform.getEnvironment(ctx.projectId, ctx.environmentId);
770
893
  const vars = { ...(current.variables ?? {}) };
@@ -777,7 +900,7 @@ async function runKeystore(action, rest) {
777
900
  const value = rest[1];
778
901
  if (!key || value === undefined) {
779
902
  console.error("Usage: ascendkit keystore set <key> <value>");
780
- process.exit(1);
903
+ return await exitCli(1);
781
904
  }
782
905
  vars[key] = value;
783
906
  await platform.updateEnvironmentVariables(ctx.projectId, ctx.environmentId, vars);
@@ -788,11 +911,11 @@ async function runKeystore(action, rest) {
788
911
  const key = rest[0];
789
912
  if (!key) {
790
913
  console.error("Usage: ascendkit keystore remove <key>");
791
- process.exit(1);
914
+ return await exitCli(1);
792
915
  }
793
916
  if (!(key in vars)) {
794
917
  console.error(`Variable "${key}" not found.`);
795
- process.exit(1);
918
+ return await exitCli(1);
796
919
  }
797
920
  delete vars[key];
798
921
  await platform.updateEnvironmentVariables(ctx.projectId, ctx.environmentId, vars);
@@ -828,7 +951,7 @@ async function runKeystore(action, rest) {
828
951
  default:
829
952
  console.error(`Unknown keystore command: ${action}`);
830
953
  console.error("Usage: ascendkit keystore list|set|remove");
831
- process.exit(1);
954
+ return await exitCli(1);
832
955
  }
833
956
  }
834
957
  async function runAuth(client, action, rest) {
@@ -866,7 +989,7 @@ async function runAuth(client, action, rest) {
866
989
  const providersArg = normalizedAction === "providers" ? rest[0] : rest[1];
867
990
  if (!providersArg) {
868
991
  console.error("Usage: ascendkit auth provider set <p1,p2,...>");
869
- process.exit(1);
992
+ return await exitCli(1);
870
993
  }
871
994
  printAuthSettingsSummary(await auth.updateProviders(client, providersArg.split(",")));
872
995
  }
@@ -885,22 +1008,22 @@ async function runAuth(client, action, rest) {
885
1008
  if (oauthAction === "set") {
886
1009
  if (!provider || !flags["client-id"]) {
887
1010
  console.error("Usage: ascendkit auth oauth set <provider> --client-id <id> [--client-secret <secret> | --client-secret-stdin] [--callback-url <url>]");
888
- process.exit(1);
1011
+ return await exitCli(1);
889
1012
  }
890
1013
  const secretFromArg = flags["client-secret"];
891
1014
  const secretFromStdin = flags["client-secret-stdin"] === "true";
892
1015
  if (!secretFromArg && !secretFromStdin) {
893
1016
  console.error("Missing client secret. Use --client-secret-stdin (recommended) or --client-secret.");
894
- process.exit(1);
1017
+ return await exitCli(1);
895
1018
  }
896
1019
  if (secretFromArg && secretFromStdin) {
897
1020
  console.error("Use only one of --client-secret or --client-secret-stdin.");
898
- process.exit(1);
1021
+ return await exitCli(1);
899
1022
  }
900
1023
  const clientSecret = secretFromArg ?? await readSecretFromStdin();
901
1024
  if (!clientSecret) {
902
1025
  console.error("Client secret cannot be empty.");
903
- process.exit(1);
1026
+ return await exitCli(1);
904
1027
  }
905
1028
  await auth.updateOAuthCredentials(client, provider, flags["client-id"], clientSecret, flags["callback-url"]);
906
1029
  console.log(`Saved OAuth credentials for ${provider}.`);
@@ -908,7 +1031,7 @@ async function runAuth(client, action, rest) {
908
1031
  else if (oauthAction === "remove") {
909
1032
  if (!provider) {
910
1033
  console.error("Usage: ascendkit auth oauth remove <provider>");
911
- process.exit(1);
1034
+ return await exitCli(1);
912
1035
  }
913
1036
  await auth.deleteOAuthCredentials(client, provider);
914
1037
  console.log(`Removed OAuth credentials for ${provider}.`);
@@ -916,7 +1039,7 @@ async function runAuth(client, action, rest) {
916
1039
  else {
917
1040
  if (!provider) {
918
1041
  console.error("Usage: ascendkit auth oauth open <provider>");
919
- process.exit(1);
1042
+ return await exitCli(1);
920
1043
  }
921
1044
  const portalUrl = process.env.ASCENDKIT_PORTAL_URL ?? "http://localhost:3000";
922
1045
  const url = auth.getOAuthSetupUrl(portalUrl, provider, client.currentPublicKey ?? undefined);
@@ -939,7 +1062,7 @@ async function runAuth(client, action, rest) {
939
1062
  else if (rest[0] === "remove") {
940
1063
  if (!rest[1]) {
941
1064
  console.error("Usage: ascendkit auth user remove <user-id>");
942
- process.exit(1);
1065
+ return await exitCli(1);
943
1066
  }
944
1067
  await auth.deleteUser(client, rest[1]);
945
1068
  console.log(`Removed user ${rest[1]}.`);
@@ -947,19 +1070,19 @@ async function runAuth(client, action, rest) {
947
1070
  else if (rest[0] === "reactivate") {
948
1071
  if (!rest[1]) {
949
1072
  console.error("Usage: ascendkit auth user reactivate <user-id>");
950
- process.exit(1);
1073
+ return await exitCli(1);
951
1074
  }
952
1075
  await auth.reactivateUser(client, rest[1]);
953
1076
  console.log(`Reactivated user ${rest[1]}.`);
954
1077
  }
955
1078
  else {
956
1079
  console.error(`Unknown auth user command: ${rest[0]}`);
957
- process.exit(1);
1080
+ return await exitCli(1);
958
1081
  }
959
1082
  break;
960
1083
  default:
961
1084
  console.error(`Unknown auth command: ${action}`);
962
- process.exit(1);
1085
+ return await exitCli(1);
963
1086
  }
964
1087
  }
965
1088
  async function runContent(client, action, rest) {
@@ -975,7 +1098,7 @@ async function runContent(client, action, rest) {
975
1098
  case "create":
976
1099
  if (!flags.name || !flags.subject || !flags["body-html"] || !flags["body-text"]) {
977
1100
  console.error("Usage: ascendkit template create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>]");
978
- process.exit(1);
1101
+ return await exitCli(1);
979
1102
  }
980
1103
  printTemplateSummary(await content.createTemplate(client, {
981
1104
  name: flags.name, subject: flags.subject,
@@ -1003,14 +1126,14 @@ async function runContent(client, action, rest) {
1003
1126
  case "show":
1004
1127
  if (!rest[0]) {
1005
1128
  console.error("Usage: ascendkit template show <template-id>");
1006
- process.exit(1);
1129
+ return await exitCli(1);
1007
1130
  }
1008
- printTemplateSummary(await content.getTemplate(client, rest[0]));
1131
+ printTemplateSummary(await content.getTemplate(client, rest[0]), { verbose: true });
1009
1132
  break;
1010
1133
  case "update":
1011
1134
  if (!rest[0]) {
1012
1135
  console.error("Usage: ascendkit template update <template-id> [--flags]");
1013
- process.exit(1);
1136
+ return await exitCli(1);
1014
1137
  }
1015
1138
  printTemplateSummary(await content.updateTemplate(client, rest[0], {
1016
1139
  subject: flags.subject,
@@ -1022,7 +1145,7 @@ async function runContent(client, action, rest) {
1022
1145
  case "remove":
1023
1146
  if (!rest[0]) {
1024
1147
  console.error("Usage: ascendkit template remove <template-id>");
1025
- process.exit(1);
1148
+ return await exitCli(1);
1026
1149
  }
1027
1150
  await content.deleteTemplate(client, rest[0]);
1028
1151
  console.log(`Removed template ${rest[0]}.`);
@@ -1033,7 +1156,7 @@ async function runContent(client, action, rest) {
1033
1156
  const templateId = normalizedAction === "versions" ? rest[0] : rest[1];
1034
1157
  if (!templateId) {
1035
1158
  console.error("Usage: ascendkit template version list <template-id>");
1036
- process.exit(1);
1159
+ return await exitCli(1);
1037
1160
  }
1038
1161
  const versions = await content.listVersions(client, templateId);
1039
1162
  table(versions, [
@@ -1047,14 +1170,14 @@ async function runContent(client, action, rest) {
1047
1170
  const versionNumber = normalizedAction === "version" && rest[0] === "show" ? rest[2] : normalizedAction === "version" ? rest[1] : rest[2];
1048
1171
  if (!templateId || !versionNumber) {
1049
1172
  console.error("Usage: ascendkit template version show <template-id> <n>");
1050
- process.exit(1);
1173
+ return await exitCli(1);
1051
1174
  }
1052
1175
  output(await content.getVersion(client, templateId, parseInt(versionNumber, 10)));
1053
1176
  }
1054
1177
  break;
1055
1178
  default:
1056
1179
  console.error(`Unknown template command: ${action}`);
1057
- process.exit(1);
1180
+ return await exitCli(1);
1058
1181
  }
1059
1182
  }
1060
1183
  async function runSurvey(client, action, rest) {
@@ -1070,7 +1193,7 @@ async function runSurvey(client, action, rest) {
1070
1193
  case "create":
1071
1194
  if (!flags.name) {
1072
1195
  console.error("Usage: ascendkit survey create --name <n> [--type nps|csat|custom]");
1073
- process.exit(1);
1196
+ return await exitCli(1);
1074
1197
  }
1075
1198
  printSurveySummary(await surveys.createSurvey(client, {
1076
1199
  name: flags.name,
@@ -1089,14 +1212,14 @@ async function runSurvey(client, action, rest) {
1089
1212
  case "show":
1090
1213
  if (!rest[0]) {
1091
1214
  console.error("Usage: ascendkit survey show <survey-id>");
1092
- process.exit(1);
1215
+ return await exitCli(1);
1093
1216
  }
1094
1217
  printSurveySummary(await surveys.getSurvey(client, rest[0]));
1095
1218
  break;
1096
1219
  case "update":
1097
1220
  if (!rest[0]) {
1098
1221
  console.error("Usage: ascendkit survey update <survey-id> [--flags]");
1099
- process.exit(1);
1222
+ return await exitCli(1);
1100
1223
  }
1101
1224
  printSurveySummary(await surveys.updateSurvey(client, rest[0], {
1102
1225
  name: flags.name,
@@ -1107,7 +1230,7 @@ async function runSurvey(client, action, rest) {
1107
1230
  case "remove":
1108
1231
  if (!rest[0]) {
1109
1232
  console.error("Usage: ascendkit survey remove <survey-id>");
1110
- process.exit(1);
1233
+ return await exitCli(1);
1111
1234
  }
1112
1235
  await surveys.deleteSurvey(client, rest[0]);
1113
1236
  console.log(`Removed survey ${rest[0]}.`);
@@ -1115,7 +1238,7 @@ async function runSurvey(client, action, rest) {
1115
1238
  case "distribute":
1116
1239
  if (!rest[0] || !flags.users) {
1117
1240
  console.error("Usage: ascendkit survey distribute <survey-id> --users <usr_id1,usr_id2,...>");
1118
- process.exit(1);
1241
+ return await exitCli(1);
1119
1242
  }
1120
1243
  output(await surveys.distributeSurvey(client, rest[0], flags.users.split(",")));
1121
1244
  break;
@@ -1125,19 +1248,19 @@ async function runSurvey(client, action, rest) {
1125
1248
  const surveyId = normalizedAction === "invitations" ? rest[0] : rest[1];
1126
1249
  if (!surveyId) {
1127
1250
  console.error("Usage: ascendkit survey invitation list <survey-id>");
1128
- process.exit(1);
1251
+ return await exitCli(1);
1129
1252
  }
1130
1253
  output(await surveys.listInvitations(client, surveyId));
1131
1254
  }
1132
1255
  else {
1133
1256
  console.error(`Unknown survey invitation command: ${rest[0]}`);
1134
- process.exit(1);
1257
+ return await exitCli(1);
1135
1258
  }
1136
1259
  break;
1137
1260
  case "analytics":
1138
1261
  if (!rest[0]) {
1139
1262
  console.error("Usage: ascendkit survey analytics <survey-id>");
1140
- process.exit(1);
1263
+ return await exitCli(1);
1141
1264
  }
1142
1265
  output(await surveys.getAnalytics(client, rest[0]));
1143
1266
  break;
@@ -1151,7 +1274,7 @@ async function runSurvey(client, action, rest) {
1151
1274
  if (definitionAction === "export") {
1152
1275
  if (!surveyId) {
1153
1276
  console.error("Usage: ascendkit survey definition export <survey-id> [--out <file>]");
1154
- process.exit(1);
1277
+ return await exitCli(1);
1155
1278
  }
1156
1279
  const survey = await surveys.getSurvey(client, surveyId);
1157
1280
  const text = `${JSON.stringify(survey.definition ?? {}, null, 2)}\n`;
@@ -1167,7 +1290,7 @@ async function runSurvey(client, action, rest) {
1167
1290
  else {
1168
1291
  if (!surveyId || !(flags.in || flags.file)) {
1169
1292
  console.error("Usage: ascendkit survey definition import <survey-id> --in <file>");
1170
- process.exit(1);
1293
+ return await exitCli(1);
1171
1294
  }
1172
1295
  const inPath = flags.in || flags.file;
1173
1296
  const raw = readFileSync(inPath, "utf8");
@@ -1177,7 +1300,7 @@ async function runSurvey(client, action, rest) {
1177
1300
  : parsed;
1178
1301
  if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
1179
1302
  console.error("Invalid definition JSON: expected an object or { definition: object }");
1180
- process.exit(1);
1303
+ return await exitCli(1);
1181
1304
  }
1182
1305
  printSurveySummary(await surveys.updateSurvey(client, surveyId, {
1183
1306
  definition: candidate,
@@ -1201,7 +1324,7 @@ async function runSurvey(client, action, rest) {
1201
1324
  const questionName = normalizedAction === "question" ? rest[2] : rest[1];
1202
1325
  if (!surveyId) {
1203
1326
  console.error("Usage: ascendkit survey question list <survey-id>");
1204
- process.exit(1);
1327
+ return await exitCli(1);
1205
1328
  }
1206
1329
  if (questionAction === "list") {
1207
1330
  output(await surveys.listQuestions(client, surveyId));
@@ -1209,7 +1332,7 @@ async function runSurvey(client, action, rest) {
1209
1332
  else if (questionAction === "add") {
1210
1333
  if (!flags.type || !flags.title) {
1211
1334
  console.error("Usage: ascendkit survey question add <survey-id> --type <type> --title <title> [--name <name>] [--required <true|false>] [--choices <c1,c2,...>] [--position <n>]");
1212
- process.exit(1);
1335
+ return await exitCli(1);
1213
1336
  }
1214
1337
  const params = { type: flags.type, title: flags.title };
1215
1338
  if (flags.name)
@@ -1225,7 +1348,7 @@ async function runSurvey(client, action, rest) {
1225
1348
  else if (questionAction === "update") {
1226
1349
  if (!questionName) {
1227
1350
  console.error("Usage: ascendkit survey question update <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]");
1228
- process.exit(1);
1351
+ return await exitCli(1);
1229
1352
  }
1230
1353
  const params = {};
1231
1354
  if (flags.title)
@@ -1239,27 +1362,27 @@ async function runSurvey(client, action, rest) {
1239
1362
  else if (questionAction === "remove") {
1240
1363
  if (!questionName) {
1241
1364
  console.error("Usage: ascendkit survey question remove <survey-id> <question-name>");
1242
- process.exit(1);
1365
+ return await exitCli(1);
1243
1366
  }
1244
1367
  output(await surveys.removeQuestion(client, surveyId, questionName));
1245
1368
  }
1246
1369
  else if (questionAction === "reorder") {
1247
1370
  if (!flags.order) {
1248
1371
  console.error("Usage: ascendkit survey question reorder <survey-id> --order <name1,name2,...>");
1249
- process.exit(1);
1372
+ return await exitCli(1);
1250
1373
  }
1251
1374
  output(await surveys.reorderQuestions(client, surveyId, flags.order.split(",")));
1252
1375
  }
1253
1376
  else {
1254
1377
  console.error(`Unknown survey question command: ${questionAction}`);
1255
- process.exit(1);
1378
+ return await exitCli(1);
1256
1379
  }
1257
1380
  break;
1258
1381
  }
1259
1382
  default:
1260
1383
  console.error(`Unknown survey command: ${action}`);
1261
1384
  console.error('Run "ascendkit survey --help" for usage');
1262
- process.exit(1);
1385
+ return await exitCli(1);
1263
1386
  }
1264
1387
  }
1265
1388
  function runStatus() {
@@ -1377,7 +1500,7 @@ async function runJourney(client, action, rest) {
1377
1500
  case "create":
1378
1501
  if (!flags.name || !flags["entry-event"] || !flags["entry-node"]) {
1379
1502
  console.error("Usage: ascendkit journey create --name <n> --entry-event <e> --entry-node <n> [--nodes <json>] [--transitions <json>] [--description <d>] [--entry-conditions <json>] [--re-entry-policy <skip|restart>]");
1380
- process.exit(1);
1503
+ return await exitCli(1);
1381
1504
  }
1382
1505
  console.log(formatJourneyWithGuidance((await journeys.createJourney(client, {
1383
1506
  name: flags.name,
@@ -1407,14 +1530,14 @@ async function runJourney(client, action, rest) {
1407
1530
  case "get":
1408
1531
  if (!rest[0]) {
1409
1532
  console.error("Usage: ascendkit journey show <journey-id>");
1410
- process.exit(1);
1533
+ return await exitCli(1);
1411
1534
  }
1412
1535
  console.log(formatJourneyWithGuidance(await journeys.getJourney(client, rest[0])));
1413
1536
  break;
1414
1537
  case "update":
1415
1538
  if (!rest[0]) {
1416
1539
  console.error("Usage: ascendkit journey update <journey-id> [--flags]");
1417
- process.exit(1);
1540
+ return await exitCli(1);
1418
1541
  }
1419
1542
  console.log(formatJourneyWithGuidance((await journeys.updateJourney(client, rest[0], {
1420
1543
  name: flags.name,
@@ -1430,7 +1553,7 @@ async function runJourney(client, action, rest) {
1430
1553
  case "delete":
1431
1554
  if (!rest[0]) {
1432
1555
  console.error("Usage: ascendkit journey remove <journey-id>");
1433
- process.exit(1);
1556
+ return await exitCli(1);
1434
1557
  }
1435
1558
  await journeys.deleteJourney(client, rest[0]);
1436
1559
  console.log(`Deleted journey ${rest[0]}.`);
@@ -1438,49 +1561,49 @@ async function runJourney(client, action, rest) {
1438
1561
  case "activate":
1439
1562
  if (!rest[0]) {
1440
1563
  console.error("Usage: ascendkit journey activate <journey-id>");
1441
- process.exit(1);
1564
+ return await exitCli(1);
1442
1565
  }
1443
1566
  console.log(formatJourneyWithGuidance(await journeys.activateJourney(client, rest[0])));
1444
1567
  break;
1445
1568
  case "pause":
1446
1569
  if (!rest[0]) {
1447
1570
  console.error("Usage: ascendkit journey pause <journey-id>");
1448
- process.exit(1);
1571
+ return await exitCli(1);
1449
1572
  }
1450
1573
  console.log(formatJourneyWithGuidance(await journeys.pauseJourney(client, rest[0])));
1451
1574
  break;
1452
1575
  case "resume":
1453
1576
  if (!rest[0]) {
1454
1577
  console.error("Usage: ascendkit journey resume <journey-id>");
1455
- process.exit(1);
1578
+ return await exitCli(1);
1456
1579
  }
1457
1580
  console.log(formatJourneyWithGuidance(await journeys.resumeJourney(client, rest[0])));
1458
1581
  break;
1459
1582
  case "archive":
1460
1583
  if (!rest[0]) {
1461
1584
  console.error("Usage: ascendkit journey archive <journey-id>");
1462
- process.exit(1);
1585
+ return await exitCli(1);
1463
1586
  }
1464
1587
  console.log(formatJourneyWithGuidance(await journeys.archiveJourney(client, rest[0])));
1465
1588
  break;
1466
1589
  case "analytics":
1467
1590
  if (!rest[0]) {
1468
1591
  console.error("Usage: ascendkit journey analytics <journey-id>");
1469
- process.exit(1);
1592
+ return await exitCli(1);
1470
1593
  }
1471
1594
  console.log(formatJourneyAnalytics(await journeys.getJourneyAnalytics(client, rest[0])));
1472
1595
  break;
1473
1596
  case "list-nodes":
1474
1597
  if (!rest[0]) {
1475
1598
  console.error("Usage: ascendkit journey node list <journey-id>");
1476
- process.exit(1);
1599
+ return await exitCli(1);
1477
1600
  }
1478
1601
  console.log(formatNodeList(await journeys.listNodes(client, rest[0])));
1479
1602
  break;
1480
1603
  case "add-node": {
1481
1604
  if (!rest[0] || !flags.name) {
1482
1605
  console.error("Usage: ascendkit journey node add <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
1483
- process.exit(1);
1606
+ return await exitCli(1);
1484
1607
  }
1485
1608
  const params = { name: flags.name };
1486
1609
  if (flags.action)
@@ -1488,7 +1611,7 @@ async function runJourney(client, action, rest) {
1488
1611
  if (flags["email-id"]) {
1489
1612
  if (!params.action || params.action.type !== "send_email") {
1490
1613
  console.error("--email-id requires a send_email action (use --action '{\"type\": \"send_email\", \"templateSlug\": \"...\"}')");
1491
- process.exit(1);
1614
+ return await exitCli(1);
1492
1615
  }
1493
1616
  params.action.fromIdentityEmail = flags["email-id"];
1494
1617
  }
@@ -1500,7 +1623,7 @@ async function runJourney(client, action, rest) {
1500
1623
  case "edit-node": {
1501
1624
  if (!rest[0] || !rest[1]) {
1502
1625
  console.error("Usage: ascendkit journey node update <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
1503
- process.exit(1);
1626
+ return await exitCli(1);
1504
1627
  }
1505
1628
  const params = {};
1506
1629
  if (flags.action)
@@ -1511,13 +1634,13 @@ async function runJourney(client, action, rest) {
1511
1634
  const current = nodesData.nodes?.find((n) => n.name === rest[1]);
1512
1635
  if (!current?.action || current.action.type !== "send_email") {
1513
1636
  console.error("--email-id can only be set on send_email nodes");
1514
- process.exit(1);
1637
+ return await exitCli(1);
1515
1638
  }
1516
1639
  params.action = current.action;
1517
1640
  }
1518
1641
  else if (params.action.type !== "send_email") {
1519
1642
  console.error("--email-id requires a send_email action");
1520
- process.exit(1);
1643
+ return await exitCli(1);
1521
1644
  }
1522
1645
  params.action.fromIdentityEmail = flags["email-id"];
1523
1646
  }
@@ -1529,14 +1652,14 @@ async function runJourney(client, action, rest) {
1529
1652
  case "remove-node":
1530
1653
  if (!rest[0] || !rest[1]) {
1531
1654
  console.error("Usage: ascendkit journey node remove <journey-id> <node-name>");
1532
- process.exit(1);
1655
+ return await exitCli(1);
1533
1656
  }
1534
1657
  console.log(formatSingleNode(await journeys.removeNode(client, rest[0], rest[1]), "Removed", rest[1]));
1535
1658
  break;
1536
1659
  case "list-transitions": {
1537
1660
  if (!rest[0]) {
1538
1661
  console.error("Usage: ascendkit journey transition list <journey-id> [--from <node-name>] [--to <node-name>]");
1539
- process.exit(1);
1662
+ return await exitCli(1);
1540
1663
  }
1541
1664
  console.log(formatTransitionList(await journeys.listTransitions(client, rest[0], {
1542
1665
  from_node: flags.from,
@@ -1547,7 +1670,7 @@ async function runJourney(client, action, rest) {
1547
1670
  case "add-transition": {
1548
1671
  if (!rest[0] || !flags.from || !flags.to || !(flags.on || flags.after || flags.trigger)) {
1549
1672
  console.error("Usage: ascendkit journey transition add <journey-id> --from <node-name> --to <node-name> --on <event> | --after <delay> | --trigger <json> [--priority <n>] [--name <transition-name>]");
1550
- process.exit(1);
1673
+ return await exitCli(1);
1551
1674
  }
1552
1675
  let trigger;
1553
1676
  if (flags.on) {
@@ -1575,7 +1698,7 @@ async function runJourney(client, action, rest) {
1575
1698
  case "edit-transition": {
1576
1699
  if (!rest[0] || !rest[1]) {
1577
1700
  console.error("Usage: ascendkit journey transition update <journey-id> <transition-name> [--on <event>] [--after <delay>] [--trigger <json>] [--priority <n>]");
1578
- process.exit(1);
1701
+ return await exitCli(1);
1579
1702
  }
1580
1703
  const params = {};
1581
1704
  if (flags.on) {
@@ -1595,13 +1718,13 @@ async function runJourney(client, action, rest) {
1595
1718
  case "remove-transition":
1596
1719
  if (!rest[0] || !rest[1]) {
1597
1720
  console.error("Usage: ascendkit journey transition remove <journey-id> <transition-name>");
1598
- process.exit(1);
1721
+ return await exitCli(1);
1599
1722
  }
1600
1723
  console.log(formatSingleTransition(await journeys.removeTransition(client, rest[0], rest[1]), "Removed", rest[1]));
1601
1724
  break;
1602
1725
  default:
1603
1726
  console.error(`Unknown journey command: ${action}`);
1604
- process.exit(1);
1727
+ return await exitCli(1);
1605
1728
  }
1606
1729
  }
1607
1730
  async function runWebhook(client, action, rest) {
@@ -1610,7 +1733,7 @@ async function runWebhook(client, action, rest) {
1610
1733
  case "create":
1611
1734
  if (!flags.url) {
1612
1735
  console.error("Usage: ascendkit webhook create --url <url> [--events <e1,e2,...>]");
1613
- process.exit(1);
1736
+ return await exitCli(1);
1614
1737
  }
1615
1738
  output(await webhooks.createWebhook(client, {
1616
1739
  url: flags.url,
@@ -1628,14 +1751,14 @@ async function runWebhook(client, action, rest) {
1628
1751
  case "get":
1629
1752
  if (!rest[0]) {
1630
1753
  console.error("Usage: ascendkit webhook get <webhook-id>");
1631
- process.exit(1);
1754
+ return await exitCli(1);
1632
1755
  }
1633
1756
  output(await webhooks.getWebhook(client, rest[0]));
1634
1757
  break;
1635
1758
  case "update": {
1636
1759
  if (!rest[0]) {
1637
1760
  console.error("Usage: ascendkit webhook update <webhook-id> [--flags]");
1638
- process.exit(1);
1761
+ return await exitCli(1);
1639
1762
  }
1640
1763
  const updated = await webhooks.updateWebhook(client, rest[0], {
1641
1764
  url: flags.url,
@@ -1651,21 +1774,21 @@ async function runWebhook(client, action, rest) {
1651
1774
  case "delete":
1652
1775
  if (!rest[0]) {
1653
1776
  console.error("Usage: ascendkit webhook delete <webhook-id>");
1654
- process.exit(1);
1777
+ return await exitCli(1);
1655
1778
  }
1656
1779
  output(await webhooks.deleteWebhook(client, rest[0]));
1657
1780
  break;
1658
1781
  case "test":
1659
1782
  if (!rest[0]) {
1660
1783
  console.error("Usage: ascendkit webhook test <webhook-id> [--event <event-type>]");
1661
- process.exit(1);
1784
+ return await exitCli(1);
1662
1785
  }
1663
1786
  output(await webhooks.testWebhook(client, rest[0], flags.event));
1664
1787
  break;
1665
1788
  default:
1666
1789
  console.error(`Unknown webhook command: ${action}`);
1667
1790
  console.error('Run "ascendkit webhook --help" for usage');
1668
- process.exit(1);
1791
+ return await exitCli(1);
1669
1792
  }
1670
1793
  }
1671
1794
  async function runCampaign(client, action, rest) {
@@ -1675,10 +1798,10 @@ async function runCampaign(client, action, rest) {
1675
1798
  return;
1676
1799
  }
1677
1800
  switch (action) {
1678
- case "create":
1801
+ case "create": {
1679
1802
  if (!flags.name || !flags.template || !flags.audience) {
1680
1803
  console.error("Usage: ascendkit campaign create --name <name> --template <template-id> --audience <json> [--scheduled-at <datetime>]");
1681
- process.exit(1);
1804
+ return await exitCli(1);
1682
1805
  }
1683
1806
  let createFilter;
1684
1807
  try {
@@ -1686,7 +1809,7 @@ async function runCampaign(client, action, rest) {
1686
1809
  }
1687
1810
  catch {
1688
1811
  console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1689
- process.exit(1);
1812
+ return await exitCli(1);
1690
1813
  }
1691
1814
  output(await campaigns.createCampaign(client, {
1692
1815
  name: flags.name,
@@ -1695,6 +1818,7 @@ async function runCampaign(client, action, rest) {
1695
1818
  scheduledAt: flags["scheduled-at"],
1696
1819
  }));
1697
1820
  break;
1821
+ }
1698
1822
  case "list": {
1699
1823
  const items = await campaigns.listCampaigns(client, flags.status);
1700
1824
  table(items, [
@@ -1710,14 +1834,14 @@ async function runCampaign(client, action, rest) {
1710
1834
  case "get":
1711
1835
  if (!rest[0]) {
1712
1836
  console.error("Usage: ascendkit campaign show <campaign-id>");
1713
- process.exit(1);
1837
+ return await exitCli(1);
1714
1838
  }
1715
1839
  output(await campaigns.getCampaign(client, rest[0]));
1716
1840
  break;
1717
- case "update":
1841
+ case "update": {
1718
1842
  if (!rest[0]) {
1719
1843
  console.error("Usage: ascendkit campaign update <campaign-id> [--flags]");
1720
- process.exit(1);
1844
+ return await exitCli(1);
1721
1845
  }
1722
1846
  let updateFilter;
1723
1847
  if (flags.audience) {
@@ -1726,7 +1850,7 @@ async function runCampaign(client, action, rest) {
1726
1850
  }
1727
1851
  catch {
1728
1852
  console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1729
- process.exit(1);
1853
+ return await exitCli(1);
1730
1854
  }
1731
1855
  }
1732
1856
  output(await campaigns.updateCampaign(client, rest[0], {
@@ -1736,45 +1860,45 @@ async function runCampaign(client, action, rest) {
1736
1860
  scheduledAt: flags["scheduled-at"],
1737
1861
  }));
1738
1862
  break;
1739
- case "preview":
1863
+ }
1864
+ case "preview": {
1740
1865
  if (!rest[0]) {
1741
1866
  console.error("Usage: ascendkit campaign preview <campaign-id>");
1742
- process.exit(1);
1867
+ return await exitCli(1);
1743
1868
  }
1744
- {
1745
- const detail = await campaigns.getCampaign(client, rest[0]);
1746
- if (!detail?.audienceFilter) {
1747
- console.error("Campaign has no audience filter set.");
1748
- process.exit(1);
1749
- }
1750
- output(await campaigns.previewAudience(client, detail.audienceFilter));
1869
+ const detail = await campaigns.getCampaign(client, rest[0]);
1870
+ if (!detail?.audienceFilter) {
1871
+ console.error("Campaign has no audience filter set.");
1872
+ return await exitCli(1);
1751
1873
  }
1874
+ output(await campaigns.previewAudience(client, detail.audienceFilter));
1752
1875
  break;
1876
+ }
1753
1877
  case "schedule":
1754
1878
  if (!rest[0] || !flags.at) {
1755
1879
  console.error("Usage: ascendkit campaign schedule <campaign-id> --at <datetime>");
1756
- process.exit(1);
1880
+ return await exitCli(1);
1757
1881
  }
1758
1882
  output(await campaigns.updateCampaign(client, rest[0], { scheduledAt: flags.at }));
1759
1883
  break;
1760
1884
  case "cancel":
1761
1885
  if (!rest[0]) {
1762
1886
  console.error("Usage: ascendkit campaign cancel <campaign-id>");
1763
- process.exit(1);
1887
+ return await exitCli(1);
1764
1888
  }
1765
1889
  output(await campaigns.deleteCampaign(client, rest[0]));
1766
1890
  break;
1767
1891
  case "analytics":
1768
1892
  if (!rest[0]) {
1769
1893
  console.error("Usage: ascendkit campaign analytics <campaign-id>");
1770
- process.exit(1);
1894
+ return await exitCli(1);
1771
1895
  }
1772
1896
  output(await campaigns.getCampaignAnalytics(client, rest[0]));
1773
1897
  break;
1774
1898
  default:
1775
1899
  console.error(`Unknown campaign command: ${action}`);
1776
1900
  console.error('Run "ascendkit campaign --help" for usage');
1777
- process.exit(1);
1901
+ return await exitCli(1);
1778
1902
  }
1779
1903
  }
1780
1904
  async function runEmail(client, action, rest) {
@@ -1797,7 +1921,7 @@ async function runEmail(client, action, rest) {
1797
1921
  case "setup-domain":
1798
1922
  if (!rest[0]) {
1799
1923
  console.error("Usage: ascendkit email-identity setup-domain <domain>");
1800
- process.exit(1);
1924
+ return await exitCli(1);
1801
1925
  }
1802
1926
  await printEmailSetup(await email.setupDomain(client, rest[0]));
1803
1927
  break;
@@ -1820,7 +1944,7 @@ async function runEmail(client, action, rest) {
1820
1944
  const url = provider?.portalUrl;
1821
1945
  if (!url) {
1822
1946
  console.error("Could not determine DNS provider URL for this domain.");
1823
- process.exit(1);
1947
+ return await exitCli(1);
1824
1948
  }
1825
1949
  console.log(url);
1826
1950
  if (flags.open === "true") {
@@ -1829,7 +1953,8 @@ async function runEmail(client, action, rest) {
1829
1953
  break;
1830
1954
  }
1831
1955
  case "remove-domain":
1832
- output(await email.removeDomain(client));
1956
+ await email.removeDomain(client);
1957
+ console.log("Domain removed.");
1833
1958
  break;
1834
1959
  case "list": {
1835
1960
  const result = await email.listIdentities(client);
@@ -1840,33 +1965,37 @@ async function runEmail(client, action, rest) {
1840
1965
  const identityEmail = rest[0];
1841
1966
  if (!identityEmail) {
1842
1967
  console.error("Usage: ascendkit email-identity add <email> [--display-name <name>]");
1843
- process.exit(1);
1968
+ return await exitCli(1);
1844
1969
  }
1845
- output(await email.createIdentity(client, {
1970
+ const added = await email.createIdentity(client, {
1846
1971
  email: identityEmail,
1847
1972
  displayName: flags["display-name"],
1848
- }));
1973
+ });
1974
+ printEmailIdentities(added.identities ?? []);
1975
+ console.log("Verification email sent.");
1849
1976
  break;
1850
1977
  }
1851
1978
  case "resend": {
1852
1979
  const identityEmail = rest[0];
1853
1980
  if (!identityEmail) {
1854
1981
  console.error("Usage: ascendkit email-identity resend <email>");
1855
- process.exit(1);
1982
+ return await exitCli(1);
1856
1983
  }
1857
- output(await email.resendIdentityVerification(client, identityEmail));
1984
+ await email.resendIdentityVerification(client, identityEmail);
1985
+ console.log(`Verification email sent to ${identityEmail}.`);
1858
1986
  break;
1859
1987
  }
1860
1988
  case "set-default": {
1861
1989
  const identityEmail = rest[0];
1862
1990
  if (!identityEmail) {
1863
1991
  console.error("Usage: ascendkit email-identity set-default <email> [--display-name <name>]");
1864
- process.exit(1);
1992
+ return await exitCli(1);
1865
1993
  }
1866
- output(await email.setDefaultIdentity(client, {
1994
+ const updated = await email.setDefaultIdentity(client, {
1867
1995
  email: identityEmail,
1868
1996
  displayName: flags["display-name"],
1869
- }));
1997
+ });
1998
+ printEmailIdentities(updated.identities ?? []);
1870
1999
  break;
1871
2000
  }
1872
2001
  case "remove":
@@ -1874,39 +2003,42 @@ async function runEmail(client, action, rest) {
1874
2003
  const identityEmail = rest[0];
1875
2004
  if (!identityEmail) {
1876
2005
  console.error("Usage: ascendkit email-identity remove <email>");
1877
- process.exit(1);
2006
+ return await exitCli(1);
1878
2007
  }
1879
- output(await email.removeIdentity(client, identityEmail));
2008
+ const remaining = await email.removeIdentity(client, identityEmail);
2009
+ console.log("Identity removed.");
2010
+ printEmailIdentities(remaining.identities ?? []);
1880
2011
  break;
1881
2012
  }
1882
2013
  case "test": {
1883
2014
  const identityEmail = rest[0];
1884
2015
  if (!identityEmail || !flags.to) {
1885
2016
  console.error("Usage: ascendkit email-identity test <email> --to <recipient>");
1886
- process.exit(1);
2017
+ return await exitCli(1);
1887
2018
  }
1888
- output(await email.sendTestEmail(client, {
2019
+ const testResult = await email.sendTestEmail(client, {
1889
2020
  to: flags.to,
1890
2021
  fromIdentityEmail: identityEmail,
1891
- }));
2022
+ });
2023
+ console.log(testResult.message ?? "Test email sent.");
1892
2024
  break;
1893
2025
  }
1894
2026
  default:
1895
2027
  console.error(`Unknown email-identity command: ${action}`);
1896
- process.exit(1);
2028
+ return await exitCli(1);
1897
2029
  }
1898
2030
  }
1899
2031
  run().catch((err) => {
1900
2032
  console.error(err.message);
1901
- process.exit(1);
2033
+ exitCli(1, err instanceof Error ? err : new Error(String(err)));
1902
2034
  });
1903
2035
  async function printEmailSetup(settings) {
1904
- output(settings);
1905
2036
  if (!settings.domain)
1906
2037
  return;
1907
2038
  const provider = settings.dnsProvider;
2039
+ console.log(`Domain: ${settings.domain}`);
1908
2040
  if (provider?.name) {
1909
- console.log(`\nDetected DNS provider: ${provider.name} (${provider.confidence ?? "unknown"})`);
2041
+ console.log(`DNS provider: ${provider.name} (${provider.confidence ?? "unknown"})`);
1910
2042
  }
1911
2043
  if (provider?.portalUrl) {
1912
2044
  console.log(`Provider console: ${provider.portalUrl}`);
@@ -1914,10 +2046,54 @@ async function printEmailSetup(settings) {
1914
2046
  if (provider?.assistantSetupUrl) {
1915
2047
  console.log(`Guided setup: ${provider.assistantSetupUrl}`);
1916
2048
  }
1917
- if (Array.isArray(settings.dnsRecords) && settings.dnsRecords.length > 0) {
1918
- console.log("\nAdd these DNS records:");
1919
- for (const rec of settings.dnsRecords) {
1920
- console.log(` ${rec.type}\t${rec.name}\t${rec.value}`);
2049
+ const records = Array.isArray(settings.dnsRecords) ? settings.dnsRecords : [];
2050
+ if (records.length > 0) {
2051
+ console.log("\nDNS records to add:");
2052
+ for (const rec of records) {
2053
+ console.log(` ${rec.type.padEnd(6)} ${rec.name} ${rec.value}`);
2054
+ }
2055
+ if (process.stdin.isTTY && process.stdout.isTTY) {
2056
+ const defaultFile = `dns-records-${settings.domain}.txt`;
2057
+ const { createInterface } = await import("node:readline/promises");
2058
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2059
+ try {
2060
+ const answer = (await rl.question(`\nSave records to file [${defaultFile}]: `)).trim();
2061
+ const filename = answer || defaultFile;
2062
+ const { existsSync, writeFileSync } = await import("node:fs");
2063
+ if (existsSync(filename)) {
2064
+ const overwrite = (await rl.question(`${filename} already exists. Overwrite? [y/N] `)).trim().toLowerCase();
2065
+ if (overwrite !== "y" && overwrite !== "yes") {
2066
+ console.log("Skipped.");
2067
+ return;
2068
+ }
2069
+ }
2070
+ const lines = [
2071
+ `; DNS records for ${settings.domain}`,
2072
+ `; Generated by AscendKit CLI`,
2073
+ "",
2074
+ ...records.map(rec => {
2075
+ let value;
2076
+ if (rec.type === "TXT") {
2077
+ value = `"${rec.value}"`;
2078
+ }
2079
+ else if (rec.type === "CNAME" || rec.type === "MX") {
2080
+ // Ensure absolute FQDN with trailing dot to prevent zone-relative expansion
2081
+ const target = rec.type === "MX" ? rec.value.replace(/^(\d+)\s+/, "$1 ") : rec.value;
2082
+ value = target.endsWith(".") ? target : `${target}.`;
2083
+ }
2084
+ else {
2085
+ value = rec.value;
2086
+ }
2087
+ const name = rec.name.endsWith(".") ? rec.name : `${rec.name}.`;
2088
+ return `${name}\t3600\tIN\t${rec.type}\t${value} ; cf_tags=cf-proxied:false`;
2089
+ }),
2090
+ ];
2091
+ writeFileSync(filename, lines.join("\n") + "\n");
2092
+ console.log(`Saved to ${filename}`);
2093
+ }
2094
+ finally {
2095
+ rl.close();
2096
+ }
1921
2097
  }
1922
2098
  }
1923
2099
  }
@@ -1949,8 +2125,20 @@ function printEmailStatusSummary(settings, domainStatus, dnsCheck, identities) {
1949
2125
  return;
1950
2126
  }
1951
2127
  console.log(`Domain: ${settings.domain} (${domainStatus.status || settings.verificationStatus || "unknown"})`);
1952
- if (dnsCheck?.summary) {
1953
- console.log(`DNS: ${dnsCheck.summary.found}/${dnsCheck.summary.total} verified`);
2128
+ if (dnsCheck) {
2129
+ const { summary, records } = dnsCheck;
2130
+ if (summary.notFound === 0 && summary.mismatch === 0 && summary.errored === 0) {
2131
+ console.log("DNS: all records verified");
2132
+ }
2133
+ else {
2134
+ console.log(`DNS: ${summary.found}/${summary.total} verified`);
2135
+ const pending = records.filter(r => !r.found || r.mismatch);
2136
+ if (pending.length > 0) {
2137
+ console.log("Pending:");
2138
+ for (const r of pending)
2139
+ console.log(` ${r.type.padEnd(6)} ${r.name}`);
2140
+ }
2141
+ }
1954
2142
  }
1955
2143
  console.log("");
1956
2144
  printEmailIdentities(identities);