@ascendkit/cli 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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})`);
@@ -348,7 +384,36 @@ function table(rows, columns) {
348
384
  }
349
385
  }
350
386
  async function run() {
387
+ installGlobalHandlers();
351
388
  const args = process.argv.slice(2);
389
+ const dtEntered = new Date();
390
+ // Extract command/domain/action for telemetry before any early returns
391
+ const teleDomain = args[0] ?? null;
392
+ const teleAction = args[1] ?? null;
393
+ const redactedArgs = redactArgs(args);
394
+ // Register telemetry hook — runs on every exit path (success or failure)
395
+ onExit(async (code, error) => {
396
+ const dtCompleted = new Date();
397
+ const record = {
398
+ invocationId: getInvocationId(),
399
+ machineId: getMachineId(),
400
+ clientType: "cli",
401
+ clientVersion: CLI_VERSION,
402
+ command: redactedArgs.join(" ") || "(empty)",
403
+ domain: teleDomain,
404
+ action: teleAction,
405
+ args: redactedArgs,
406
+ dtEntered: dtEntered.toISOString(),
407
+ dtCompleted: dtCompleted.toISOString(),
408
+ durationMs: dtCompleted.getTime() - dtEntered.getTime(),
409
+ success: code === 0,
410
+ errorMessage: error?.message ?? null,
411
+ hostname: osHostname(),
412
+ os: osPlatform(),
413
+ nodeVersion: process.version,
414
+ };
415
+ await captureTelemetry(record);
416
+ });
352
417
  if (args[0] === "--version" || args[0] === "-v" || args[0] === "-V") {
353
418
  console.log(CLI_VERSION);
354
419
  return;
@@ -362,7 +427,7 @@ async function run() {
362
427
  if (!printSectionHelp(args[1])) {
363
428
  console.error(`Unknown help section: ${args[1]}`);
364
429
  console.error('Run "ascendkit --help" for available sections.');
365
- process.exit(1);
430
+ return await exitCli(1);
366
431
  }
367
432
  return;
368
433
  }
@@ -378,7 +443,7 @@ async function run() {
378
443
  if (!printSectionHelp(domain)) {
379
444
  console.error(`Unknown command section: ${domain}`);
380
445
  console.error('Run "ascendkit --help" for usage');
381
- process.exit(1);
446
+ return await exitCli(1);
382
447
  }
383
448
  return;
384
449
  }
@@ -404,7 +469,7 @@ async function run() {
404
469
  else if (action === "show") {
405
470
  if (!args[2]) {
406
471
  console.error("Usage: ascendkit project show <project-id>");
407
- process.exit(1);
472
+ return await exitCli(1);
408
473
  }
409
474
  printProjectSummary(await platform.showProject(args[2]));
410
475
  }
@@ -415,7 +480,7 @@ async function run() {
415
480
  const flags = parseFlags(args.slice(2));
416
481
  if (!flags.name) {
417
482
  console.error("Usage: ascendkit project create --name <name> [--description <description>] [--services <s1,s2,...>]");
418
- process.exit(1);
483
+ return await exitCli(1);
419
484
  }
420
485
  try {
421
486
  const proj = await platform.createProject(flags.name, flags.description, flags.services?.split(","));
@@ -435,18 +500,18 @@ async function run() {
435
500
  catch { /* use raw message */ }
436
501
  }
437
502
  console.error(message);
438
- process.exit(1);
503
+ return await exitCli(1);
439
504
  }
440
505
  }
441
506
  else {
442
507
  console.error('Usage: ascendkit project list|create|show|env');
443
- process.exit(1);
508
+ return await exitCli(1);
444
509
  }
445
510
  return;
446
511
  case "set-env":
447
512
  if (!action) {
448
513
  console.error("Usage: ascendkit set-env <public-key>");
449
- process.exit(1);
514
+ return await exitCli(1);
450
515
  }
451
516
  await platform.setEnv(action);
452
517
  return;
@@ -495,7 +560,7 @@ async function run() {
495
560
  default:
496
561
  console.error(`Unknown command: ${domain}`);
497
562
  console.error('Run "ascendkit --help" for usage');
498
- process.exit(1);
563
+ return await exitCli(1);
499
564
  }
500
565
  }
501
566
  async function runImport(client, source, rest) {
@@ -529,7 +594,7 @@ async function runImport(client, source, rest) {
529
594
  console.error(`Unsupported import source: ${source}`);
530
595
  console.error("Supported sources: clerk");
531
596
  console.error('Run "ascendkit help import" for usage');
532
- process.exit(1);
597
+ return await exitCli(1);
533
598
  }
534
599
  const dryRun = action !== "run";
535
600
  const hasUsers = flags.users !== undefined;
@@ -542,7 +607,7 @@ async function runImport(client, source, rest) {
542
607
  const filePath = flags.file;
543
608
  if (!apiKey && !filePath) {
544
609
  console.error("Usage: ascendkit import clerk preview|run --api-key <key> | --file <path>");
545
- process.exit(1);
610
+ return await exitCli(1);
546
611
  }
547
612
  let clerkUsers = [];
548
613
  if (filePath) {
@@ -668,7 +733,7 @@ async function runProjectEnvironment(rest) {
668
733
  case "list":
669
734
  if (!target) {
670
735
  console.error("Usage: ascendkit project env list <project-id>");
671
- process.exit(1);
736
+ return await exitCli(1);
672
737
  }
673
738
  table(await platform.listEnvironments(target), [
674
739
  { key: "id", label: "ID" },
@@ -679,7 +744,7 @@ async function runProjectEnvironment(rest) {
679
744
  return;
680
745
  default:
681
746
  console.error("Usage: ascendkit project env list <project-id>");
682
- process.exit(1);
747
+ return await exitCli(1);
683
748
  }
684
749
  }
685
750
  async function runEnvironment(action, rest) {
@@ -687,7 +752,7 @@ async function runEnvironment(action, rest) {
687
752
  const ctx = loadEnvContext();
688
753
  if (!ctx) {
689
754
  console.error("No environment set. Run: ascendkit set-env <public-key>");
690
- process.exit(1);
755
+ return await exitCli(1);
691
756
  }
692
757
  switch (action) {
693
758
  case "show":
@@ -698,7 +763,7 @@ async function runEnvironment(action, rest) {
698
763
  const target = flags.target;
699
764
  if (!envId || !target) {
700
765
  console.error("Usage: ascendkit environment promote [<env-id>] --target <tier>");
701
- process.exit(1);
766
+ return await exitCli(1);
702
767
  }
703
768
  try {
704
769
  const result = await platform.promoteEnvironment(envId, target);
@@ -719,7 +784,7 @@ async function runEnvironment(action, rest) {
719
784
  catch { /* use raw message */ }
720
785
  }
721
786
  console.error(message);
722
- process.exit(1);
787
+ return await exitCli(1);
723
788
  }
724
789
  return;
725
790
  }
@@ -729,7 +794,7 @@ async function runEnvironment(action, rest) {
729
794
  const description = flags.description;
730
795
  if (!name && description === undefined) {
731
796
  console.error("Provide at least --name or --description to update.");
732
- process.exit(1);
797
+ return await exitCli(1);
733
798
  }
734
799
  try {
735
800
  const result = await platform.updateEnvironment(ctx.projectId, envId, name, description);
@@ -750,21 +815,21 @@ async function runEnvironment(action, rest) {
750
815
  catch { /* use raw message */ }
751
816
  }
752
817
  console.error(message);
753
- process.exit(1);
818
+ return await exitCli(1);
754
819
  }
755
820
  return;
756
821
  }
757
822
  default:
758
823
  console.error(`Unknown environment command: ${action}`);
759
824
  console.error("Usage: ascendkit environment show|update|promote");
760
- process.exit(1);
825
+ return await exitCli(1);
761
826
  }
762
827
  }
763
828
  async function runKeystore(action, rest) {
764
829
  const ctx = loadEnvContext();
765
830
  if (!ctx) {
766
831
  console.error("No environment set. Run: ascendkit set-env <public-key>");
767
- process.exit(1);
832
+ return await exitCli(1);
768
833
  }
769
834
  const current = await platform.getEnvironment(ctx.projectId, ctx.environmentId);
770
835
  const vars = { ...(current.variables ?? {}) };
@@ -777,7 +842,7 @@ async function runKeystore(action, rest) {
777
842
  const value = rest[1];
778
843
  if (!key || value === undefined) {
779
844
  console.error("Usage: ascendkit keystore set <key> <value>");
780
- process.exit(1);
845
+ return await exitCli(1);
781
846
  }
782
847
  vars[key] = value;
783
848
  await platform.updateEnvironmentVariables(ctx.projectId, ctx.environmentId, vars);
@@ -788,11 +853,11 @@ async function runKeystore(action, rest) {
788
853
  const key = rest[0];
789
854
  if (!key) {
790
855
  console.error("Usage: ascendkit keystore remove <key>");
791
- process.exit(1);
856
+ return await exitCli(1);
792
857
  }
793
858
  if (!(key in vars)) {
794
859
  console.error(`Variable "${key}" not found.`);
795
- process.exit(1);
860
+ return await exitCli(1);
796
861
  }
797
862
  delete vars[key];
798
863
  await platform.updateEnvironmentVariables(ctx.projectId, ctx.environmentId, vars);
@@ -828,7 +893,7 @@ async function runKeystore(action, rest) {
828
893
  default:
829
894
  console.error(`Unknown keystore command: ${action}`);
830
895
  console.error("Usage: ascendkit keystore list|set|remove");
831
- process.exit(1);
896
+ return await exitCli(1);
832
897
  }
833
898
  }
834
899
  async function runAuth(client, action, rest) {
@@ -866,7 +931,7 @@ async function runAuth(client, action, rest) {
866
931
  const providersArg = normalizedAction === "providers" ? rest[0] : rest[1];
867
932
  if (!providersArg) {
868
933
  console.error("Usage: ascendkit auth provider set <p1,p2,...>");
869
- process.exit(1);
934
+ return await exitCli(1);
870
935
  }
871
936
  printAuthSettingsSummary(await auth.updateProviders(client, providersArg.split(",")));
872
937
  }
@@ -885,22 +950,22 @@ async function runAuth(client, action, rest) {
885
950
  if (oauthAction === "set") {
886
951
  if (!provider || !flags["client-id"]) {
887
952
  console.error("Usage: ascendkit auth oauth set <provider> --client-id <id> [--client-secret <secret> | --client-secret-stdin] [--callback-url <url>]");
888
- process.exit(1);
953
+ return await exitCli(1);
889
954
  }
890
955
  const secretFromArg = flags["client-secret"];
891
956
  const secretFromStdin = flags["client-secret-stdin"] === "true";
892
957
  if (!secretFromArg && !secretFromStdin) {
893
958
  console.error("Missing client secret. Use --client-secret-stdin (recommended) or --client-secret.");
894
- process.exit(1);
959
+ return await exitCli(1);
895
960
  }
896
961
  if (secretFromArg && secretFromStdin) {
897
962
  console.error("Use only one of --client-secret or --client-secret-stdin.");
898
- process.exit(1);
963
+ return await exitCli(1);
899
964
  }
900
965
  const clientSecret = secretFromArg ?? await readSecretFromStdin();
901
966
  if (!clientSecret) {
902
967
  console.error("Client secret cannot be empty.");
903
- process.exit(1);
968
+ return await exitCli(1);
904
969
  }
905
970
  await auth.updateOAuthCredentials(client, provider, flags["client-id"], clientSecret, flags["callback-url"]);
906
971
  console.log(`Saved OAuth credentials for ${provider}.`);
@@ -908,7 +973,7 @@ async function runAuth(client, action, rest) {
908
973
  else if (oauthAction === "remove") {
909
974
  if (!provider) {
910
975
  console.error("Usage: ascendkit auth oauth remove <provider>");
911
- process.exit(1);
976
+ return await exitCli(1);
912
977
  }
913
978
  await auth.deleteOAuthCredentials(client, provider);
914
979
  console.log(`Removed OAuth credentials for ${provider}.`);
@@ -916,7 +981,7 @@ async function runAuth(client, action, rest) {
916
981
  else {
917
982
  if (!provider) {
918
983
  console.error("Usage: ascendkit auth oauth open <provider>");
919
- process.exit(1);
984
+ return await exitCli(1);
920
985
  }
921
986
  const portalUrl = process.env.ASCENDKIT_PORTAL_URL ?? "http://localhost:3000";
922
987
  const url = auth.getOAuthSetupUrl(portalUrl, provider, client.currentPublicKey ?? undefined);
@@ -939,7 +1004,7 @@ async function runAuth(client, action, rest) {
939
1004
  else if (rest[0] === "remove") {
940
1005
  if (!rest[1]) {
941
1006
  console.error("Usage: ascendkit auth user remove <user-id>");
942
- process.exit(1);
1007
+ return await exitCli(1);
943
1008
  }
944
1009
  await auth.deleteUser(client, rest[1]);
945
1010
  console.log(`Removed user ${rest[1]}.`);
@@ -947,19 +1012,19 @@ async function runAuth(client, action, rest) {
947
1012
  else if (rest[0] === "reactivate") {
948
1013
  if (!rest[1]) {
949
1014
  console.error("Usage: ascendkit auth user reactivate <user-id>");
950
- process.exit(1);
1015
+ return await exitCli(1);
951
1016
  }
952
1017
  await auth.reactivateUser(client, rest[1]);
953
1018
  console.log(`Reactivated user ${rest[1]}.`);
954
1019
  }
955
1020
  else {
956
1021
  console.error(`Unknown auth user command: ${rest[0]}`);
957
- process.exit(1);
1022
+ return await exitCli(1);
958
1023
  }
959
1024
  break;
960
1025
  default:
961
1026
  console.error(`Unknown auth command: ${action}`);
962
- process.exit(1);
1027
+ return await exitCli(1);
963
1028
  }
964
1029
  }
965
1030
  async function runContent(client, action, rest) {
@@ -975,7 +1040,7 @@ async function runContent(client, action, rest) {
975
1040
  case "create":
976
1041
  if (!flags.name || !flags.subject || !flags["body-html"] || !flags["body-text"]) {
977
1042
  console.error("Usage: ascendkit template create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>]");
978
- process.exit(1);
1043
+ return await exitCli(1);
979
1044
  }
980
1045
  printTemplateSummary(await content.createTemplate(client, {
981
1046
  name: flags.name, subject: flags.subject,
@@ -1003,14 +1068,14 @@ async function runContent(client, action, rest) {
1003
1068
  case "show":
1004
1069
  if (!rest[0]) {
1005
1070
  console.error("Usage: ascendkit template show <template-id>");
1006
- process.exit(1);
1071
+ return await exitCli(1);
1007
1072
  }
1008
- printTemplateSummary(await content.getTemplate(client, rest[0]));
1073
+ printTemplateSummary(await content.getTemplate(client, rest[0]), { verbose: true });
1009
1074
  break;
1010
1075
  case "update":
1011
1076
  if (!rest[0]) {
1012
1077
  console.error("Usage: ascendkit template update <template-id> [--flags]");
1013
- process.exit(1);
1078
+ return await exitCli(1);
1014
1079
  }
1015
1080
  printTemplateSummary(await content.updateTemplate(client, rest[0], {
1016
1081
  subject: flags.subject,
@@ -1022,7 +1087,7 @@ async function runContent(client, action, rest) {
1022
1087
  case "remove":
1023
1088
  if (!rest[0]) {
1024
1089
  console.error("Usage: ascendkit template remove <template-id>");
1025
- process.exit(1);
1090
+ return await exitCli(1);
1026
1091
  }
1027
1092
  await content.deleteTemplate(client, rest[0]);
1028
1093
  console.log(`Removed template ${rest[0]}.`);
@@ -1033,7 +1098,7 @@ async function runContent(client, action, rest) {
1033
1098
  const templateId = normalizedAction === "versions" ? rest[0] : rest[1];
1034
1099
  if (!templateId) {
1035
1100
  console.error("Usage: ascendkit template version list <template-id>");
1036
- process.exit(1);
1101
+ return await exitCli(1);
1037
1102
  }
1038
1103
  const versions = await content.listVersions(client, templateId);
1039
1104
  table(versions, [
@@ -1047,14 +1112,14 @@ async function runContent(client, action, rest) {
1047
1112
  const versionNumber = normalizedAction === "version" && rest[0] === "show" ? rest[2] : normalizedAction === "version" ? rest[1] : rest[2];
1048
1113
  if (!templateId || !versionNumber) {
1049
1114
  console.error("Usage: ascendkit template version show <template-id> <n>");
1050
- process.exit(1);
1115
+ return await exitCli(1);
1051
1116
  }
1052
1117
  output(await content.getVersion(client, templateId, parseInt(versionNumber, 10)));
1053
1118
  }
1054
1119
  break;
1055
1120
  default:
1056
1121
  console.error(`Unknown template command: ${action}`);
1057
- process.exit(1);
1122
+ return await exitCli(1);
1058
1123
  }
1059
1124
  }
1060
1125
  async function runSurvey(client, action, rest) {
@@ -1070,7 +1135,7 @@ async function runSurvey(client, action, rest) {
1070
1135
  case "create":
1071
1136
  if (!flags.name) {
1072
1137
  console.error("Usage: ascendkit survey create --name <n> [--type nps|csat|custom]");
1073
- process.exit(1);
1138
+ return await exitCli(1);
1074
1139
  }
1075
1140
  printSurveySummary(await surveys.createSurvey(client, {
1076
1141
  name: flags.name,
@@ -1089,14 +1154,14 @@ async function runSurvey(client, action, rest) {
1089
1154
  case "show":
1090
1155
  if (!rest[0]) {
1091
1156
  console.error("Usage: ascendkit survey show <survey-id>");
1092
- process.exit(1);
1157
+ return await exitCli(1);
1093
1158
  }
1094
1159
  printSurveySummary(await surveys.getSurvey(client, rest[0]));
1095
1160
  break;
1096
1161
  case "update":
1097
1162
  if (!rest[0]) {
1098
1163
  console.error("Usage: ascendkit survey update <survey-id> [--flags]");
1099
- process.exit(1);
1164
+ return await exitCli(1);
1100
1165
  }
1101
1166
  printSurveySummary(await surveys.updateSurvey(client, rest[0], {
1102
1167
  name: flags.name,
@@ -1107,7 +1172,7 @@ async function runSurvey(client, action, rest) {
1107
1172
  case "remove":
1108
1173
  if (!rest[0]) {
1109
1174
  console.error("Usage: ascendkit survey remove <survey-id>");
1110
- process.exit(1);
1175
+ return await exitCli(1);
1111
1176
  }
1112
1177
  await surveys.deleteSurvey(client, rest[0]);
1113
1178
  console.log(`Removed survey ${rest[0]}.`);
@@ -1115,7 +1180,7 @@ async function runSurvey(client, action, rest) {
1115
1180
  case "distribute":
1116
1181
  if (!rest[0] || !flags.users) {
1117
1182
  console.error("Usage: ascendkit survey distribute <survey-id> --users <usr_id1,usr_id2,...>");
1118
- process.exit(1);
1183
+ return await exitCli(1);
1119
1184
  }
1120
1185
  output(await surveys.distributeSurvey(client, rest[0], flags.users.split(",")));
1121
1186
  break;
@@ -1125,19 +1190,19 @@ async function runSurvey(client, action, rest) {
1125
1190
  const surveyId = normalizedAction === "invitations" ? rest[0] : rest[1];
1126
1191
  if (!surveyId) {
1127
1192
  console.error("Usage: ascendkit survey invitation list <survey-id>");
1128
- process.exit(1);
1193
+ return await exitCli(1);
1129
1194
  }
1130
1195
  output(await surveys.listInvitations(client, surveyId));
1131
1196
  }
1132
1197
  else {
1133
1198
  console.error(`Unknown survey invitation command: ${rest[0]}`);
1134
- process.exit(1);
1199
+ return await exitCli(1);
1135
1200
  }
1136
1201
  break;
1137
1202
  case "analytics":
1138
1203
  if (!rest[0]) {
1139
1204
  console.error("Usage: ascendkit survey analytics <survey-id>");
1140
- process.exit(1);
1205
+ return await exitCli(1);
1141
1206
  }
1142
1207
  output(await surveys.getAnalytics(client, rest[0]));
1143
1208
  break;
@@ -1151,7 +1216,7 @@ async function runSurvey(client, action, rest) {
1151
1216
  if (definitionAction === "export") {
1152
1217
  if (!surveyId) {
1153
1218
  console.error("Usage: ascendkit survey definition export <survey-id> [--out <file>]");
1154
- process.exit(1);
1219
+ return await exitCli(1);
1155
1220
  }
1156
1221
  const survey = await surveys.getSurvey(client, surveyId);
1157
1222
  const text = `${JSON.stringify(survey.definition ?? {}, null, 2)}\n`;
@@ -1167,7 +1232,7 @@ async function runSurvey(client, action, rest) {
1167
1232
  else {
1168
1233
  if (!surveyId || !(flags.in || flags.file)) {
1169
1234
  console.error("Usage: ascendkit survey definition import <survey-id> --in <file>");
1170
- process.exit(1);
1235
+ return await exitCli(1);
1171
1236
  }
1172
1237
  const inPath = flags.in || flags.file;
1173
1238
  const raw = readFileSync(inPath, "utf8");
@@ -1177,7 +1242,7 @@ async function runSurvey(client, action, rest) {
1177
1242
  : parsed;
1178
1243
  if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
1179
1244
  console.error("Invalid definition JSON: expected an object or { definition: object }");
1180
- process.exit(1);
1245
+ return await exitCli(1);
1181
1246
  }
1182
1247
  printSurveySummary(await surveys.updateSurvey(client, surveyId, {
1183
1248
  definition: candidate,
@@ -1201,7 +1266,7 @@ async function runSurvey(client, action, rest) {
1201
1266
  const questionName = normalizedAction === "question" ? rest[2] : rest[1];
1202
1267
  if (!surveyId) {
1203
1268
  console.error("Usage: ascendkit survey question list <survey-id>");
1204
- process.exit(1);
1269
+ return await exitCli(1);
1205
1270
  }
1206
1271
  if (questionAction === "list") {
1207
1272
  output(await surveys.listQuestions(client, surveyId));
@@ -1209,7 +1274,7 @@ async function runSurvey(client, action, rest) {
1209
1274
  else if (questionAction === "add") {
1210
1275
  if (!flags.type || !flags.title) {
1211
1276
  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);
1277
+ return await exitCli(1);
1213
1278
  }
1214
1279
  const params = { type: flags.type, title: flags.title };
1215
1280
  if (flags.name)
@@ -1225,7 +1290,7 @@ async function runSurvey(client, action, rest) {
1225
1290
  else if (questionAction === "update") {
1226
1291
  if (!questionName) {
1227
1292
  console.error("Usage: ascendkit survey question update <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]");
1228
- process.exit(1);
1293
+ return await exitCli(1);
1229
1294
  }
1230
1295
  const params = {};
1231
1296
  if (flags.title)
@@ -1239,27 +1304,27 @@ async function runSurvey(client, action, rest) {
1239
1304
  else if (questionAction === "remove") {
1240
1305
  if (!questionName) {
1241
1306
  console.error("Usage: ascendkit survey question remove <survey-id> <question-name>");
1242
- process.exit(1);
1307
+ return await exitCli(1);
1243
1308
  }
1244
1309
  output(await surveys.removeQuestion(client, surveyId, questionName));
1245
1310
  }
1246
1311
  else if (questionAction === "reorder") {
1247
1312
  if (!flags.order) {
1248
1313
  console.error("Usage: ascendkit survey question reorder <survey-id> --order <name1,name2,...>");
1249
- process.exit(1);
1314
+ return await exitCli(1);
1250
1315
  }
1251
1316
  output(await surveys.reorderQuestions(client, surveyId, flags.order.split(",")));
1252
1317
  }
1253
1318
  else {
1254
1319
  console.error(`Unknown survey question command: ${questionAction}`);
1255
- process.exit(1);
1320
+ return await exitCli(1);
1256
1321
  }
1257
1322
  break;
1258
1323
  }
1259
1324
  default:
1260
1325
  console.error(`Unknown survey command: ${action}`);
1261
1326
  console.error('Run "ascendkit survey --help" for usage');
1262
- process.exit(1);
1327
+ return await exitCli(1);
1263
1328
  }
1264
1329
  }
1265
1330
  function runStatus() {
@@ -1377,7 +1442,7 @@ async function runJourney(client, action, rest) {
1377
1442
  case "create":
1378
1443
  if (!flags.name || !flags["entry-event"] || !flags["entry-node"]) {
1379
1444
  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);
1445
+ return await exitCli(1);
1381
1446
  }
1382
1447
  console.log(formatJourneyWithGuidance((await journeys.createJourney(client, {
1383
1448
  name: flags.name,
@@ -1407,14 +1472,14 @@ async function runJourney(client, action, rest) {
1407
1472
  case "get":
1408
1473
  if (!rest[0]) {
1409
1474
  console.error("Usage: ascendkit journey show <journey-id>");
1410
- process.exit(1);
1475
+ return await exitCli(1);
1411
1476
  }
1412
1477
  console.log(formatJourneyWithGuidance(await journeys.getJourney(client, rest[0])));
1413
1478
  break;
1414
1479
  case "update":
1415
1480
  if (!rest[0]) {
1416
1481
  console.error("Usage: ascendkit journey update <journey-id> [--flags]");
1417
- process.exit(1);
1482
+ return await exitCli(1);
1418
1483
  }
1419
1484
  console.log(formatJourneyWithGuidance((await journeys.updateJourney(client, rest[0], {
1420
1485
  name: flags.name,
@@ -1430,7 +1495,7 @@ async function runJourney(client, action, rest) {
1430
1495
  case "delete":
1431
1496
  if (!rest[0]) {
1432
1497
  console.error("Usage: ascendkit journey remove <journey-id>");
1433
- process.exit(1);
1498
+ return await exitCli(1);
1434
1499
  }
1435
1500
  await journeys.deleteJourney(client, rest[0]);
1436
1501
  console.log(`Deleted journey ${rest[0]}.`);
@@ -1438,49 +1503,49 @@ async function runJourney(client, action, rest) {
1438
1503
  case "activate":
1439
1504
  if (!rest[0]) {
1440
1505
  console.error("Usage: ascendkit journey activate <journey-id>");
1441
- process.exit(1);
1506
+ return await exitCli(1);
1442
1507
  }
1443
1508
  console.log(formatJourneyWithGuidance(await journeys.activateJourney(client, rest[0])));
1444
1509
  break;
1445
1510
  case "pause":
1446
1511
  if (!rest[0]) {
1447
1512
  console.error("Usage: ascendkit journey pause <journey-id>");
1448
- process.exit(1);
1513
+ return await exitCli(1);
1449
1514
  }
1450
1515
  console.log(formatJourneyWithGuidance(await journeys.pauseJourney(client, rest[0])));
1451
1516
  break;
1452
1517
  case "resume":
1453
1518
  if (!rest[0]) {
1454
1519
  console.error("Usage: ascendkit journey resume <journey-id>");
1455
- process.exit(1);
1520
+ return await exitCli(1);
1456
1521
  }
1457
1522
  console.log(formatJourneyWithGuidance(await journeys.resumeJourney(client, rest[0])));
1458
1523
  break;
1459
1524
  case "archive":
1460
1525
  if (!rest[0]) {
1461
1526
  console.error("Usage: ascendkit journey archive <journey-id>");
1462
- process.exit(1);
1527
+ return await exitCli(1);
1463
1528
  }
1464
1529
  console.log(formatJourneyWithGuidance(await journeys.archiveJourney(client, rest[0])));
1465
1530
  break;
1466
1531
  case "analytics":
1467
1532
  if (!rest[0]) {
1468
1533
  console.error("Usage: ascendkit journey analytics <journey-id>");
1469
- process.exit(1);
1534
+ return await exitCli(1);
1470
1535
  }
1471
1536
  console.log(formatJourneyAnalytics(await journeys.getJourneyAnalytics(client, rest[0])));
1472
1537
  break;
1473
1538
  case "list-nodes":
1474
1539
  if (!rest[0]) {
1475
1540
  console.error("Usage: ascendkit journey node list <journey-id>");
1476
- process.exit(1);
1541
+ return await exitCli(1);
1477
1542
  }
1478
1543
  console.log(formatNodeList(await journeys.listNodes(client, rest[0])));
1479
1544
  break;
1480
1545
  case "add-node": {
1481
1546
  if (!rest[0] || !flags.name) {
1482
1547
  console.error("Usage: ascendkit journey node add <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
1483
- process.exit(1);
1548
+ return await exitCli(1);
1484
1549
  }
1485
1550
  const params = { name: flags.name };
1486
1551
  if (flags.action)
@@ -1488,7 +1553,7 @@ async function runJourney(client, action, rest) {
1488
1553
  if (flags["email-id"]) {
1489
1554
  if (!params.action || params.action.type !== "send_email") {
1490
1555
  console.error("--email-id requires a send_email action (use --action '{\"type\": \"send_email\", \"templateSlug\": \"...\"}')");
1491
- process.exit(1);
1556
+ return await exitCli(1);
1492
1557
  }
1493
1558
  params.action.fromIdentityEmail = flags["email-id"];
1494
1559
  }
@@ -1500,7 +1565,7 @@ async function runJourney(client, action, rest) {
1500
1565
  case "edit-node": {
1501
1566
  if (!rest[0] || !rest[1]) {
1502
1567
  console.error("Usage: ascendkit journey node update <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
1503
- process.exit(1);
1568
+ return await exitCli(1);
1504
1569
  }
1505
1570
  const params = {};
1506
1571
  if (flags.action)
@@ -1511,13 +1576,13 @@ async function runJourney(client, action, rest) {
1511
1576
  const current = nodesData.nodes?.find((n) => n.name === rest[1]);
1512
1577
  if (!current?.action || current.action.type !== "send_email") {
1513
1578
  console.error("--email-id can only be set on send_email nodes");
1514
- process.exit(1);
1579
+ return await exitCli(1);
1515
1580
  }
1516
1581
  params.action = current.action;
1517
1582
  }
1518
1583
  else if (params.action.type !== "send_email") {
1519
1584
  console.error("--email-id requires a send_email action");
1520
- process.exit(1);
1585
+ return await exitCli(1);
1521
1586
  }
1522
1587
  params.action.fromIdentityEmail = flags["email-id"];
1523
1588
  }
@@ -1529,14 +1594,14 @@ async function runJourney(client, action, rest) {
1529
1594
  case "remove-node":
1530
1595
  if (!rest[0] || !rest[1]) {
1531
1596
  console.error("Usage: ascendkit journey node remove <journey-id> <node-name>");
1532
- process.exit(1);
1597
+ return await exitCli(1);
1533
1598
  }
1534
1599
  console.log(formatSingleNode(await journeys.removeNode(client, rest[0], rest[1]), "Removed", rest[1]));
1535
1600
  break;
1536
1601
  case "list-transitions": {
1537
1602
  if (!rest[0]) {
1538
1603
  console.error("Usage: ascendkit journey transition list <journey-id> [--from <node-name>] [--to <node-name>]");
1539
- process.exit(1);
1604
+ return await exitCli(1);
1540
1605
  }
1541
1606
  console.log(formatTransitionList(await journeys.listTransitions(client, rest[0], {
1542
1607
  from_node: flags.from,
@@ -1547,7 +1612,7 @@ async function runJourney(client, action, rest) {
1547
1612
  case "add-transition": {
1548
1613
  if (!rest[0] || !flags.from || !flags.to || !(flags.on || flags.after || flags.trigger)) {
1549
1614
  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);
1615
+ return await exitCli(1);
1551
1616
  }
1552
1617
  let trigger;
1553
1618
  if (flags.on) {
@@ -1575,7 +1640,7 @@ async function runJourney(client, action, rest) {
1575
1640
  case "edit-transition": {
1576
1641
  if (!rest[0] || !rest[1]) {
1577
1642
  console.error("Usage: ascendkit journey transition update <journey-id> <transition-name> [--on <event>] [--after <delay>] [--trigger <json>] [--priority <n>]");
1578
- process.exit(1);
1643
+ return await exitCli(1);
1579
1644
  }
1580
1645
  const params = {};
1581
1646
  if (flags.on) {
@@ -1595,13 +1660,13 @@ async function runJourney(client, action, rest) {
1595
1660
  case "remove-transition":
1596
1661
  if (!rest[0] || !rest[1]) {
1597
1662
  console.error("Usage: ascendkit journey transition remove <journey-id> <transition-name>");
1598
- process.exit(1);
1663
+ return await exitCli(1);
1599
1664
  }
1600
1665
  console.log(formatSingleTransition(await journeys.removeTransition(client, rest[0], rest[1]), "Removed", rest[1]));
1601
1666
  break;
1602
1667
  default:
1603
1668
  console.error(`Unknown journey command: ${action}`);
1604
- process.exit(1);
1669
+ return await exitCli(1);
1605
1670
  }
1606
1671
  }
1607
1672
  async function runWebhook(client, action, rest) {
@@ -1610,7 +1675,7 @@ async function runWebhook(client, action, rest) {
1610
1675
  case "create":
1611
1676
  if (!flags.url) {
1612
1677
  console.error("Usage: ascendkit webhook create --url <url> [--events <e1,e2,...>]");
1613
- process.exit(1);
1678
+ return await exitCli(1);
1614
1679
  }
1615
1680
  output(await webhooks.createWebhook(client, {
1616
1681
  url: flags.url,
@@ -1628,14 +1693,14 @@ async function runWebhook(client, action, rest) {
1628
1693
  case "get":
1629
1694
  if (!rest[0]) {
1630
1695
  console.error("Usage: ascendkit webhook get <webhook-id>");
1631
- process.exit(1);
1696
+ return await exitCli(1);
1632
1697
  }
1633
1698
  output(await webhooks.getWebhook(client, rest[0]));
1634
1699
  break;
1635
1700
  case "update": {
1636
1701
  if (!rest[0]) {
1637
1702
  console.error("Usage: ascendkit webhook update <webhook-id> [--flags]");
1638
- process.exit(1);
1703
+ return await exitCli(1);
1639
1704
  }
1640
1705
  const updated = await webhooks.updateWebhook(client, rest[0], {
1641
1706
  url: flags.url,
@@ -1651,21 +1716,21 @@ async function runWebhook(client, action, rest) {
1651
1716
  case "delete":
1652
1717
  if (!rest[0]) {
1653
1718
  console.error("Usage: ascendkit webhook delete <webhook-id>");
1654
- process.exit(1);
1719
+ return await exitCli(1);
1655
1720
  }
1656
1721
  output(await webhooks.deleteWebhook(client, rest[0]));
1657
1722
  break;
1658
1723
  case "test":
1659
1724
  if (!rest[0]) {
1660
1725
  console.error("Usage: ascendkit webhook test <webhook-id> [--event <event-type>]");
1661
- process.exit(1);
1726
+ return await exitCli(1);
1662
1727
  }
1663
1728
  output(await webhooks.testWebhook(client, rest[0], flags.event));
1664
1729
  break;
1665
1730
  default:
1666
1731
  console.error(`Unknown webhook command: ${action}`);
1667
1732
  console.error('Run "ascendkit webhook --help" for usage');
1668
- process.exit(1);
1733
+ return await exitCli(1);
1669
1734
  }
1670
1735
  }
1671
1736
  async function runCampaign(client, action, rest) {
@@ -1675,10 +1740,10 @@ async function runCampaign(client, action, rest) {
1675
1740
  return;
1676
1741
  }
1677
1742
  switch (action) {
1678
- case "create":
1743
+ case "create": {
1679
1744
  if (!flags.name || !flags.template || !flags.audience) {
1680
1745
  console.error("Usage: ascendkit campaign create --name <name> --template <template-id> --audience <json> [--scheduled-at <datetime>]");
1681
- process.exit(1);
1746
+ return await exitCli(1);
1682
1747
  }
1683
1748
  let createFilter;
1684
1749
  try {
@@ -1686,7 +1751,7 @@ async function runCampaign(client, action, rest) {
1686
1751
  }
1687
1752
  catch {
1688
1753
  console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1689
- process.exit(1);
1754
+ return await exitCli(1);
1690
1755
  }
1691
1756
  output(await campaigns.createCampaign(client, {
1692
1757
  name: flags.name,
@@ -1695,6 +1760,7 @@ async function runCampaign(client, action, rest) {
1695
1760
  scheduledAt: flags["scheduled-at"],
1696
1761
  }));
1697
1762
  break;
1763
+ }
1698
1764
  case "list": {
1699
1765
  const items = await campaigns.listCampaigns(client, flags.status);
1700
1766
  table(items, [
@@ -1710,14 +1776,14 @@ async function runCampaign(client, action, rest) {
1710
1776
  case "get":
1711
1777
  if (!rest[0]) {
1712
1778
  console.error("Usage: ascendkit campaign show <campaign-id>");
1713
- process.exit(1);
1779
+ return await exitCli(1);
1714
1780
  }
1715
1781
  output(await campaigns.getCampaign(client, rest[0]));
1716
1782
  break;
1717
- case "update":
1783
+ case "update": {
1718
1784
  if (!rest[0]) {
1719
1785
  console.error("Usage: ascendkit campaign update <campaign-id> [--flags]");
1720
- process.exit(1);
1786
+ return await exitCli(1);
1721
1787
  }
1722
1788
  let updateFilter;
1723
1789
  if (flags.audience) {
@@ -1726,7 +1792,7 @@ async function runCampaign(client, action, rest) {
1726
1792
  }
1727
1793
  catch {
1728
1794
  console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1729
- process.exit(1);
1795
+ return await exitCli(1);
1730
1796
  }
1731
1797
  }
1732
1798
  output(await campaigns.updateCampaign(client, rest[0], {
@@ -1736,45 +1802,45 @@ async function runCampaign(client, action, rest) {
1736
1802
  scheduledAt: flags["scheduled-at"],
1737
1803
  }));
1738
1804
  break;
1739
- case "preview":
1805
+ }
1806
+ case "preview": {
1740
1807
  if (!rest[0]) {
1741
1808
  console.error("Usage: ascendkit campaign preview <campaign-id>");
1742
- process.exit(1);
1809
+ return await exitCli(1);
1743
1810
  }
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));
1811
+ const detail = await campaigns.getCampaign(client, rest[0]);
1812
+ if (!detail?.audienceFilter) {
1813
+ console.error("Campaign has no audience filter set.");
1814
+ return await exitCli(1);
1751
1815
  }
1816
+ output(await campaigns.previewAudience(client, detail.audienceFilter));
1752
1817
  break;
1818
+ }
1753
1819
  case "schedule":
1754
1820
  if (!rest[0] || !flags.at) {
1755
1821
  console.error("Usage: ascendkit campaign schedule <campaign-id> --at <datetime>");
1756
- process.exit(1);
1822
+ return await exitCli(1);
1757
1823
  }
1758
1824
  output(await campaigns.updateCampaign(client, rest[0], { scheduledAt: flags.at }));
1759
1825
  break;
1760
1826
  case "cancel":
1761
1827
  if (!rest[0]) {
1762
1828
  console.error("Usage: ascendkit campaign cancel <campaign-id>");
1763
- process.exit(1);
1829
+ return await exitCli(1);
1764
1830
  }
1765
1831
  output(await campaigns.deleteCampaign(client, rest[0]));
1766
1832
  break;
1767
1833
  case "analytics":
1768
1834
  if (!rest[0]) {
1769
1835
  console.error("Usage: ascendkit campaign analytics <campaign-id>");
1770
- process.exit(1);
1836
+ return await exitCli(1);
1771
1837
  }
1772
1838
  output(await campaigns.getCampaignAnalytics(client, rest[0]));
1773
1839
  break;
1774
1840
  default:
1775
1841
  console.error(`Unknown campaign command: ${action}`);
1776
1842
  console.error('Run "ascendkit campaign --help" for usage');
1777
- process.exit(1);
1843
+ return await exitCli(1);
1778
1844
  }
1779
1845
  }
1780
1846
  async function runEmail(client, action, rest) {
@@ -1797,7 +1863,7 @@ async function runEmail(client, action, rest) {
1797
1863
  case "setup-domain":
1798
1864
  if (!rest[0]) {
1799
1865
  console.error("Usage: ascendkit email-identity setup-domain <domain>");
1800
- process.exit(1);
1866
+ return await exitCli(1);
1801
1867
  }
1802
1868
  await printEmailSetup(await email.setupDomain(client, rest[0]));
1803
1869
  break;
@@ -1820,7 +1886,7 @@ async function runEmail(client, action, rest) {
1820
1886
  const url = provider?.portalUrl;
1821
1887
  if (!url) {
1822
1888
  console.error("Could not determine DNS provider URL for this domain.");
1823
- process.exit(1);
1889
+ return await exitCli(1);
1824
1890
  }
1825
1891
  console.log(url);
1826
1892
  if (flags.open === "true") {
@@ -1840,7 +1906,7 @@ async function runEmail(client, action, rest) {
1840
1906
  const identityEmail = rest[0];
1841
1907
  if (!identityEmail) {
1842
1908
  console.error("Usage: ascendkit email-identity add <email> [--display-name <name>]");
1843
- process.exit(1);
1909
+ return await exitCli(1);
1844
1910
  }
1845
1911
  output(await email.createIdentity(client, {
1846
1912
  email: identityEmail,
@@ -1852,7 +1918,7 @@ async function runEmail(client, action, rest) {
1852
1918
  const identityEmail = rest[0];
1853
1919
  if (!identityEmail) {
1854
1920
  console.error("Usage: ascendkit email-identity resend <email>");
1855
- process.exit(1);
1921
+ return await exitCli(1);
1856
1922
  }
1857
1923
  output(await email.resendIdentityVerification(client, identityEmail));
1858
1924
  break;
@@ -1861,7 +1927,7 @@ async function runEmail(client, action, rest) {
1861
1927
  const identityEmail = rest[0];
1862
1928
  if (!identityEmail) {
1863
1929
  console.error("Usage: ascendkit email-identity set-default <email> [--display-name <name>]");
1864
- process.exit(1);
1930
+ return await exitCli(1);
1865
1931
  }
1866
1932
  output(await email.setDefaultIdentity(client, {
1867
1933
  email: identityEmail,
@@ -1874,7 +1940,7 @@ async function runEmail(client, action, rest) {
1874
1940
  const identityEmail = rest[0];
1875
1941
  if (!identityEmail) {
1876
1942
  console.error("Usage: ascendkit email-identity remove <email>");
1877
- process.exit(1);
1943
+ return await exitCli(1);
1878
1944
  }
1879
1945
  output(await email.removeIdentity(client, identityEmail));
1880
1946
  break;
@@ -1883,7 +1949,7 @@ async function runEmail(client, action, rest) {
1883
1949
  const identityEmail = rest[0];
1884
1950
  if (!identityEmail || !flags.to) {
1885
1951
  console.error("Usage: ascendkit email-identity test <email> --to <recipient>");
1886
- process.exit(1);
1952
+ return await exitCli(1);
1887
1953
  }
1888
1954
  output(await email.sendTestEmail(client, {
1889
1955
  to: flags.to,
@@ -1893,12 +1959,12 @@ async function runEmail(client, action, rest) {
1893
1959
  }
1894
1960
  default:
1895
1961
  console.error(`Unknown email-identity command: ${action}`);
1896
- process.exit(1);
1962
+ return await exitCli(1);
1897
1963
  }
1898
1964
  }
1899
1965
  run().catch((err) => {
1900
1966
  console.error(err.message);
1901
- process.exit(1);
1967
+ exitCli(1, err instanceof Error ? err : new Error(String(err)));
1902
1968
  });
1903
1969
  async function printEmailSetup(settings) {
1904
1970
  output(settings);