@ascendkit/cli 0.2.6 → 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,12 @@ 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";
22
+ import { formatJourneyAnalytics, formatJourneyWithGuidance, formatNodeList, formatSingleNode, formatSingleTransition, formatTransitionList, } from "./utils/journey-format.js";
17
23
  const require = createRequire(import.meta.url);
18
24
  const { version: CLI_VERSION } = require("../package.json");
19
25
  const HELP = `ascendkit v${CLI_VERSION} - AscendKit CLI
@@ -29,17 +35,18 @@ Getting Started:
29
35
 
30
36
  Services:
31
37
  auth Authentication, providers, OAuth, users
32
- templates Email templates and versioning
38
+ template Email templates and versioning
33
39
  survey Surveys, questions, distribution, analytics
34
40
  journey Lifecycle journeys, nodes, transitions
35
- email Email settings, domain verification, DNS
41
+ email-identity Email domains, sender identities, and DNS
42
+ keystore Environment runtime key-value settings
36
43
  webhook Webhook endpoints and testing
37
44
  campaign Email campaigns, scheduling, analytics
38
45
  import Import users from external auth providers
39
46
 
40
47
  Project Management:
41
- projects List and create projects
42
- env List, switch, update, and promote environments
48
+ project Projects and environment selection
49
+ environment Active environment operations
43
50
  verify Check all services in the active environment
44
51
 
45
52
  Run "ascendkit help <section>" for detailed command usage.
@@ -48,70 +55,79 @@ const HELP_SECTION = {
48
55
  auth: `Usage: ascendkit auth <command>
49
56
 
50
57
  Commands:
51
- auth settings
52
- auth settings update --providers <p1,p2,...> [--email-verification <true|false>] [--waitlist <true|false>] [--password-reset <true|false>] [--session-duration <duration>]
53
- auth providers <p1,p2,...>
54
- auth oauth <provider>
58
+ auth show
59
+ auth update [--providers <p1,p2,...>] [--email-verification <true|false>] [--waitlist <true|false>] [--password-reset <true|false>] [--session-duration <duration>]
60
+ auth provider list
61
+ auth provider set <p1,p2,...>
62
+ auth oauth open <provider>
55
63
  auth oauth set <provider> --client-id <id> [--client-secret <secret> | --client-secret-stdin] [--callback-url <url>]
56
- auth users`,
57
- templates: `Usage: ascendkit templates <command>
64
+ auth oauth remove <provider>
65
+ auth user list
66
+ auth user remove <user-id>
67
+ auth user reactivate <user-id>`,
68
+ template: `Usage: ascendkit template <command>
58
69
 
59
70
  Commands:
60
- templates create --name <name> --subject <subject> --body-html <html> --body-text <text> [--slug <slug>] [--description <description>]
61
- templates list [--query <search>] [--system true|--custom true]
62
- templates get <template-id>
63
- templates update <template-id> [--subject <subject>] [--body-html <html>] [--body-text <text>] [--change-note <note>]
64
- templates delete <template-id>
65
- templates versions <template-id>
66
- templates version <template-id> <n>`,
71
+ template create --name <name> --subject <subject> --body-html <html> --body-text <text> [--slug <slug>] [--description <description>]
72
+ template list [--query <search>] [--system true|--custom true]
73
+ template show <template-id>
74
+ template update <template-id> [--subject <subject>] [--body-html <html>] [--body-text <text>] [--change-note <note>]
75
+ template remove <template-id>
76
+ template version list <template-id>
77
+ template version show <template-id> <n>`,
67
78
  survey: `Usage: ascendkit survey <command>
68
79
 
69
80
  Commands:
70
81
  survey create --name <name> [--type <nps|csat|custom>] [--definition <json>]
71
82
  survey list
72
- survey get <survey-id>
83
+ survey show <survey-id>
73
84
  survey update <survey-id> [--name <name>] [--status <draft|active|paused>] [--definition <json>]
74
- survey delete <survey-id>
85
+ survey remove <survey-id>
75
86
  survey distribute <survey-id> --users <usr_id1,usr_id2,...>
76
- survey invitations <survey-id>
87
+ survey invitation list <survey-id>
77
88
  survey analytics <survey-id>
78
- survey export-definition <survey-id> [--out <file>]
79
- survey import-definition <survey-id> --in <file>
80
-
81
- Notes:
82
- - import-definition only updates the survey definition. It does not mutate slug/name/status.`,
89
+ survey definition export <survey-id> [--out <file>]
90
+ survey definition import <survey-id> --in <file>
91
+ survey question list <survey-id>
92
+ survey question add <survey-id> --type <type> --title <title> [--name <name>] [--required <true|false>] [--choices <c1,c2,...>] [--position <n>]
93
+ survey question update <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]
94
+ survey question remove <survey-id> <question-name>
95
+ survey question reorder <survey-id> --order <name1,name2,...>`,
83
96
  journey: `Usage: ascendkit journey <command>
84
97
 
85
98
  Commands:
86
99
  journey create --name <name> --entry-event <event> --entry-node <node> [--nodes <json>] [--transitions <json>] [--description <description>] [--entry-conditions <json>] [--re-entry-policy <skip|restart>]
87
100
  journey list [--status <draft|active|paused|archived>]
88
- journey get <journey-id>
101
+ journey show <journey-id>
89
102
  journey update <journey-id> [--name <name>] [--nodes <json>] [--transitions <json>] [--description <description>] [--entry-event <event>] [--entry-node <node>] [--entry-conditions <json>] [--re-entry-policy <skip|restart>]
90
- journey delete <journey-id>
103
+ journey remove <journey-id>
91
104
  journey activate <journey-id>
92
105
  journey pause <journey-id>
106
+ journey resume <journey-id>
93
107
  journey archive <journey-id>
94
108
  journey analytics <journey-id>
95
- journey list-nodes <journey-id>
96
- journey add-node <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
97
- journey edit-node <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
98
- journey remove-node <journey-id> <node-name>
99
- journey list-transitions <journey-id> [--from <node-name>] [--to <node-name>]
100
- journey add-transition <journey-id> --from <node-name> --to <node-name> --trigger <json> [--priority <n>] [--name <transition-name>]
101
- journey edit-transition <journey-id> <transition-name> [--trigger <json>] [--priority <n>]
102
- journey remove-transition <journey-id> <transition-name>`,
103
- email: `Usage: ascendkit email <command>
109
+ journey node list <journey-id>
110
+ journey node add <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
111
+ journey node update <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
112
+ journey node remove <journey-id> <node-name>
113
+ journey transition list <journey-id> [--from <node-name>] [--to <node-name>]
114
+ journey transition add <journey-id> --from <node-name> --to <node-name> --trigger <json> [--priority <n>] [--name <transition-name>]
115
+ journey transition update <journey-id> <transition-name> [--trigger <json>] [--priority <n>]
116
+ journey transition remove <journey-id> <transition-name>`,
117
+ "email-identity": `Usage: ascendkit email-identity <command>
104
118
 
105
119
  Commands:
106
- email settings
107
- email settings update [--from-email <email>] [--from-name <name>]
108
- email identity
109
- email use-default
110
- email use-custom <domain> [--from-email <email>] [--from-name <name>]
111
- email setup-domain <domain>
112
- email domain-status [--watch] [--interval <seconds>]
113
- email open-dns [--domain <domain>] [--open]
114
- email remove-domain`,
120
+ email-identity settings [--json]
121
+ email-identity setup-domain <domain>
122
+ email-identity status [--watch] [--interval <seconds>]
123
+ email-identity remove-domain
124
+ email-identity list
125
+ email-identity add <email> [--display-name <name>]
126
+ email-identity resend <email>
127
+ email-identity set-default <email> [--display-name <name>]
128
+ email-identity remove <email>
129
+ email-identity test <email> --to <recipient>
130
+ email-identity open-dns [--domain <domain>] [--open]`,
115
131
  webhook: `Usage: ascendkit webhook <command>
116
132
 
117
133
  Commands:
@@ -137,50 +153,61 @@ Notes:
137
153
  - --audience is a JSON filter object, e.g. '{"tags":{"$in":["premium"]}}'
138
154
  - --scheduled-at / --at accepts ISO 8601 datetime, e.g. 2026-03-15T10:00:00Z
139
155
  - cancel deletes a draft/failed campaign or cancels a scheduled/sending campaign`,
140
- env: `Usage: ascendkit env <command>
156
+ environment: `Usage: ascendkit environment <command>
157
+
158
+ Commands:
159
+ environment show
160
+ environment update [<env-id>] [--name <name>] [--description <desc>]
161
+ environment promote [<env-id>] --target <tier>`,
162
+ keystore: `Usage: ascendkit keystore <command>
141
163
 
142
164
  Commands:
143
- env list --project <project-id>
144
- env use <tier> --project <project-id>
145
- env update <env-id> --project <project-id> [--name <name>] [--description <desc>]
146
- env promote <env-id> --target <tier>
147
- env set-var <key> <value>
148
- env unset-var <key>
149
- env list-vars`,
150
- import: `Usage: ascendkit import <source> [options]
165
+ keystore list
166
+ keystore set <key> <value>
167
+ keystore remove <key>`,
168
+ import: `Usage: ascendkit import <source> <action> [options]
151
169
 
152
170
  Sources:
153
- clerk Import users from Clerk
171
+ clerk Import users from Clerk
172
+ migration-journey Create migration email templates and journeys
154
173
 
155
174
  Commands:
156
- import clerk --api-key <key> [options]
157
- import clerk --file <path> [options]
158
- import create-migration-journey [--from-identity <email>]
175
+ import clerk preview --api-key <key> [options]
176
+ import clerk preview --file <path> [options]
177
+ import clerk run --api-key <key> [options]
178
+ import clerk run --file <path> [options]
179
+ import migration-journey create [--from-identity <email>]
159
180
 
160
181
  Options:
161
182
  --api-key <key> Clerk secret API key (fetches users from Clerk API)
162
183
  --file <path> Path to Clerk dashboard export (JSON)
163
184
  --instance-url <url> Custom Clerk API URL (default: https://api.clerk.com)
164
- --execute Run the import for real (default is dry-run preview)
165
185
  --users Import users (included by default; use to select only users)
166
186
  --settings Import auth settings / OAuth providers (included by default)
167
187
  --from-identity <email> Email identity for migration journey emails
168
188
 
169
- By default, import runs in dry-run mode and includes both users and settings.
170
- Pass --execute to apply changes. Pass --users or --settings alone to select
171
- only that phase (e.g. --users --execute imports only users, not settings).`,
172
- projects: `Usage: ascendkit projects <command>
189
+ Use "preview" for dry-run mode and "run" to apply changes. Pass --users or --settings alone to select
190
+ only that phase (e.g. "import clerk run --users ..." imports only users, not settings).`,
191
+ project: `Usage: ascendkit project <command>
173
192
 
174
193
  Commands:
175
- projects list
176
- projects create --name <name> [--description <description>] [--services <s1,s2,...>]`,
194
+ project list
195
+ project create --name <name> [--description <description>] [--services <s1,s2,...>]
196
+ project show <project-id>
197
+ project env list <project-id>`,
177
198
  };
178
199
  function printSectionHelp(section) {
179
200
  if (!section)
180
201
  return false;
181
202
  let key = section.toLowerCase();
182
- if (key === "content")
183
- key = "templates";
203
+ if (key === "content" || key === "templates")
204
+ key = "template";
205
+ if (key === "email")
206
+ key = "email-identity";
207
+ if (key === "projects")
208
+ key = "project";
209
+ if (key === "env" || key === "environments")
210
+ key = "environment";
184
211
  const text = HELP_SECTION[key];
185
212
  if (!text)
186
213
  return false;
@@ -188,14 +215,15 @@ function printSectionHelp(section) {
188
215
  return true;
189
216
  }
190
217
  function getClient() {
191
- let publicKey = process.env.ASCENDKIT_PUBLIC_KEY;
218
+ let publicKey = process.env.ASCENDKIT_ENV_KEY;
192
219
  let apiUrl = process.env.ASCENDKIT_API_URL;
193
220
  const auth = loadAuth();
194
221
  const env = loadEnvContext();
195
222
  // Require auth unless env var provides the public key (CI/CD escape hatch)
196
223
  if (!auth?.token && !publicKey) {
197
224
  console.error("Not initialized. Run: ascendkit init");
198
- process.exit(1);
225
+ exitCli(1);
226
+ throw new Error("unreachable");
199
227
  }
200
228
  if (!publicKey && env?.publicKey) {
201
229
  publicKey = env.publicKey;
@@ -205,7 +233,8 @@ function getClient() {
205
233
  }
206
234
  if (!publicKey) {
207
235
  console.error("No environment set. Run: ascendkit set-env <public-key>");
208
- process.exit(1);
236
+ exitCli(1);
237
+ throw new Error("unreachable");
209
238
  }
210
239
  const client = new AscendKitClient({
211
240
  apiUrl: apiUrl ?? DEFAULT_API_URL,
@@ -241,6 +270,84 @@ async function readSecretFromStdin() {
241
270
  function output(data) {
242
271
  console.log(JSON.stringify(data, null, 2));
243
272
  }
273
+ function printAuthSettingsSummary(data) {
274
+ const providers = Array.isArray(data.providers) ? data.providers : [];
275
+ const features = data.features ?? {};
276
+ const featureLines = [
277
+ ["Email verification", features.emailVerification],
278
+ ["Waitlist", features.waitlist],
279
+ ["Password reset", features.passwordReset],
280
+ ["Require username", features.requireUsername],
281
+ ];
282
+ console.log(`Providers: ${providers.length > 0 ? providers.join(", ") : "none"}`);
283
+ for (const [label, enabled] of featureLines) {
284
+ console.log(`${label}: ${enabled ? "on" : "off"}`);
285
+ }
286
+ if (data.sessionDuration) {
287
+ console.log(`Session: ${data.sessionDuration}`);
288
+ }
289
+ }
290
+ function printTemplateSummary(data, opts) {
291
+ console.log(`Template: ${data.name} (${data.id})`);
292
+ if (data.slug)
293
+ console.log(`Slug: ${data.slug}`);
294
+ console.log(`Subject: ${data.subject ?? "-"}`);
295
+ if (data.currentVersion != null)
296
+ console.log(`Version: ${data.currentVersion}`);
297
+ if (Array.isArray(data.variables) && data.variables.length > 0) {
298
+ console.log(`Variables: ${data.variables.join(", ")}`);
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
+ }
329
+ }
330
+ function printSurveySummary(data) {
331
+ console.log(`Survey: ${data.name} (${data.id})`);
332
+ console.log(`Type: ${data.type ?? "custom"} | Status: ${data.status ?? "draft"}`);
333
+ if (data.slug)
334
+ console.log(`Slug: ${data.slug}`);
335
+ const questions = Array.isArray(data.definition?.pages)
336
+ ? data.definition.pages.flatMap((page) => page.elements ?? []).length
337
+ : undefined;
338
+ if (questions != null)
339
+ console.log(`Questions: ${questions}`);
340
+ }
341
+ function printProjectSummary(data) {
342
+ console.log(`Project: ${data.id}`);
343
+ console.log(`Name: ${data.name}`);
344
+ if (Array.isArray(data.enabledServices) && data.enabledServices.length > 0) {
345
+ console.log(`Services: ${data.enabledServices.join(", ")}`);
346
+ }
347
+ if (data.environment?.publicKey) {
348
+ console.log(`Environment: ${data.environment.publicKey}`);
349
+ }
350
+ }
244
351
  function normalizeJourneyRows(data) {
245
352
  if (Array.isArray(data)) {
246
353
  return data;
@@ -277,7 +384,36 @@ function table(rows, columns) {
277
384
  }
278
385
  }
279
386
  async function run() {
387
+ installGlobalHandlers();
280
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
+ });
281
417
  if (args[0] === "--version" || args[0] === "-v" || args[0] === "-V") {
282
418
  console.log(CLI_VERSION);
283
419
  return;
@@ -291,7 +427,7 @@ async function run() {
291
427
  if (!printSectionHelp(args[1])) {
292
428
  console.error(`Unknown help section: ${args[1]}`);
293
429
  console.error('Run "ascendkit --help" for available sections.');
294
- process.exit(1);
430
+ return await exitCli(1);
295
431
  }
296
432
  return;
297
433
  }
@@ -307,7 +443,7 @@ async function run() {
307
443
  if (!printSectionHelp(domain)) {
308
444
  console.error(`Unknown command section: ${domain}`);
309
445
  console.error('Run "ascendkit --help" for usage');
310
- process.exit(1);
446
+ return await exitCli(1);
311
447
  }
312
448
  return;
313
449
  }
@@ -321,7 +457,7 @@ async function run() {
321
457
  case "logout":
322
458
  platform.logout();
323
459
  return;
324
- case "projects":
460
+ case "project":
325
461
  if (action === "list") {
326
462
  const projects = await platform.listProjects();
327
463
  table(projects, [
@@ -330,18 +466,25 @@ async function run() {
330
466
  { key: "enabledServices", label: "Services", width: 30 },
331
467
  ]);
332
468
  }
469
+ else if (action === "show") {
470
+ if (!args[2]) {
471
+ console.error("Usage: ascendkit project show <project-id>");
472
+ return await exitCli(1);
473
+ }
474
+ printProjectSummary(await platform.showProject(args[2]));
475
+ }
476
+ else if (action === "env") {
477
+ await runProjectEnvironment(args.slice(2));
478
+ }
333
479
  else if (action === "create") {
334
480
  const flags = parseFlags(args.slice(2));
335
481
  if (!flags.name) {
336
- console.error("Usage: ascendkit projects create --name <name> [--description <description>] [--services <s1,s2,...>]");
337
- process.exit(1);
482
+ console.error("Usage: ascendkit project create --name <name> [--description <description>] [--services <s1,s2,...>]");
483
+ return await exitCli(1);
338
484
  }
339
485
  try {
340
486
  const proj = await platform.createProject(flags.name, flags.description, flags.services?.split(","));
341
- const env = proj.environment;
342
- console.log(`Project created: ${proj.id}`);
343
- if (env)
344
- console.log(`Environment: ${env.publicKey}`);
487
+ printProjectSummary(proj);
345
488
  }
346
489
  catch (err) {
347
490
  let message = err instanceof Error ? err.message : String(err);
@@ -357,18 +500,18 @@ async function run() {
357
500
  catch { /* use raw message */ }
358
501
  }
359
502
  console.error(message);
360
- process.exit(1);
503
+ return await exitCli(1);
361
504
  }
362
505
  }
363
506
  else {
364
- console.error('Usage: ascendkit projects list|create');
365
- process.exit(1);
507
+ console.error('Usage: ascendkit project list|create|show|env');
508
+ return await exitCli(1);
366
509
  }
367
510
  return;
368
511
  case "set-env":
369
512
  if (!action) {
370
513
  console.error("Usage: ascendkit set-env <public-key>");
371
- process.exit(1);
514
+ return await exitCli(1);
372
515
  }
373
516
  await platform.setEnv(action);
374
517
  return;
@@ -378,8 +521,11 @@ async function run() {
378
521
  case "verify":
379
522
  await runVerify();
380
523
  return;
381
- case "env":
382
- await runEnv(action, args.slice(2));
524
+ case "environment":
525
+ await runEnvironment(action, args.slice(2));
526
+ return;
527
+ case "keystore":
528
+ await runKeystore(action, args.slice(2));
383
529
  return;
384
530
  }
385
531
  // Service commands (need environment key)
@@ -388,6 +534,7 @@ async function run() {
388
534
  case "auth":
389
535
  await runAuth(client, action, args.slice(2));
390
536
  break;
537
+ case "template":
391
538
  case "templates":
392
539
  case "content":
393
540
  await runContent(client, action, args.slice(2));
@@ -398,7 +545,7 @@ async function run() {
398
545
  case "journey":
399
546
  await runJourney(client, action, args.slice(2));
400
547
  break;
401
- case "email":
548
+ case "email-identity":
402
549
  await runEmail(client, action, args.slice(2));
403
550
  break;
404
551
  case "webhook":
@@ -413,17 +560,33 @@ async function run() {
413
560
  default:
414
561
  console.error(`Unknown command: ${domain}`);
415
562
  console.error('Run "ascendkit --help" for usage');
416
- process.exit(1);
563
+ return await exitCli(1);
417
564
  }
418
565
  }
419
566
  async function runImport(client, source, rest) {
420
- const flags = parseFlags(rest);
567
+ if (!source) {
568
+ console.log(HELP_SECTION.import);
569
+ return;
570
+ }
571
+ let action = rest[0];
572
+ let args = rest;
421
573
  if (source === "create-migration-journey") {
574
+ source = "migration-journey";
575
+ action = "create";
576
+ }
577
+ if (source === "clerk" && (action === "preview" || action === "run")) {
578
+ args = rest.slice(1);
579
+ }
580
+ else if (source === "clerk") {
581
+ action = flagsFromLegacy(rest).execute ? "run" : "preview";
582
+ }
583
+ const flags = parseFlags(args);
584
+ if (source === "migration-journey" && action === "create") {
422
585
  const result = await importCmd.instantiateMigrationJourney(client, flags["from-identity"]);
423
586
  output(result);
424
587
  console.log("\nNext steps:");
425
588
  console.log(" ascendkit journey list — review created journeys");
426
- console.log(" ascendkit templates list — review migration email templates");
589
+ console.log(" ascendkit template list — review migration email templates");
427
590
  console.log(" ascendkit journey activate <journey-id> — activate when ready");
428
591
  return;
429
592
  }
@@ -431,10 +594,9 @@ async function runImport(client, source, rest) {
431
594
  console.error(`Unsupported import source: ${source}`);
432
595
  console.error("Supported sources: clerk");
433
596
  console.error('Run "ascendkit help import" for usage');
434
- process.exit(1);
597
+ return await exitCli(1);
435
598
  }
436
- const execute = flags.execute === "true" || flags.execute === "";
437
- const dryRun = !execute;
599
+ const dryRun = action !== "run";
438
600
  const hasUsers = flags.users !== undefined;
439
601
  const hasSettings = flags.settings !== undefined;
440
602
  // If neither --users nor --settings is passed, both default to true.
@@ -444,8 +606,8 @@ async function runImport(client, source, rest) {
444
606
  const apiKey = flags["api-key"];
445
607
  const filePath = flags.file;
446
608
  if (!apiKey && !filePath) {
447
- console.error("Usage: ascendkit import clerk --api-key <key> | --file <path> [--execute]");
448
- process.exit(1);
609
+ console.error("Usage: ascendkit import clerk preview|run --api-key <key> | --file <path>");
610
+ return await exitCli(1);
449
611
  }
450
612
  let clerkUsers = [];
451
613
  if (filePath) {
@@ -558,36 +720,50 @@ async function runImport(client, source, rest) {
558
720
  }
559
721
  else if (totalImported > 0) {
560
722
  console.log("\nTo set up migration emails, run:\n" +
561
- " ascendkit import create-migration-journey");
723
+ " ascendkit import migration-journey create");
562
724
  }
563
725
  }
564
- async function runEnv(action, rest) {
565
- const flags = parseFlags(rest);
726
+ function flagsFromLegacy(args) {
727
+ return parseFlags(args);
728
+ }
729
+ async function runProjectEnvironment(rest) {
730
+ const action = rest[0];
731
+ const target = rest[1];
566
732
  switch (action) {
567
733
  case "list":
568
- if (!flags.project) {
569
- console.error("Usage: ascendkit env list --project <project-id>");
570
- process.exit(1);
734
+ if (!target) {
735
+ console.error("Usage: ascendkit project env list <project-id>");
736
+ return await exitCli(1);
571
737
  }
572
- table(await platform.listEnvironments(flags.project), [
738
+ table(await platform.listEnvironments(target), [
739
+ { key: "id", label: "ID" },
573
740
  { key: "name", label: "Name", width: 20 },
574
741
  { key: "tier", label: "Tier" },
575
742
  { key: "publicKey", label: "Public Key" },
576
743
  ]);
577
- break;
578
- case "use":
579
- if (!rest[0] || !flags.project) {
580
- console.error("Usage: ascendkit env use <tier> --project <project-id>");
581
- process.exit(1);
582
- }
583
- await platform.useEnvironment(rest[0], flags.project);
584
- break;
744
+ return;
745
+ default:
746
+ console.error("Usage: ascendkit project env list <project-id>");
747
+ return await exitCli(1);
748
+ }
749
+ }
750
+ async function runEnvironment(action, rest) {
751
+ const flags = parseFlags(rest);
752
+ const ctx = loadEnvContext();
753
+ if (!ctx) {
754
+ console.error("No environment set. Run: ascendkit set-env <public-key>");
755
+ return await exitCli(1);
756
+ }
757
+ switch (action) {
758
+ case "show":
759
+ output(await platform.getEnvironment(ctx.projectId, ctx.environmentId));
760
+ return;
585
761
  case "promote": {
586
- const envId = rest[0];
762
+ const envId = rest[0] && !rest[0].startsWith("--") ? rest[0] : ctx.environmentId;
587
763
  const target = flags.target;
588
764
  if (!envId || !target) {
589
- console.error("Usage: ascendkit env promote <env-id> --target <tier>");
590
- process.exit(1);
765
+ console.error("Usage: ascendkit environment promote [<env-id>] --target <tier>");
766
+ return await exitCli(1);
591
767
  }
592
768
  try {
593
769
  const result = await platform.promoteEnvironment(envId, target);
@@ -608,24 +784,20 @@ async function runEnv(action, rest) {
608
784
  catch { /* use raw message */ }
609
785
  }
610
786
  console.error(message);
611
- process.exit(1);
787
+ return await exitCli(1);
612
788
  }
613
- break;
789
+ return;
614
790
  }
615
791
  case "update": {
616
- const envId = rest[0];
617
- if (!envId || !flags.project) {
618
- console.error("Usage: ascendkit env update <env-id> --project <project-id> [--name <name>] [--description <desc>]");
619
- process.exit(1);
620
- }
792
+ const envId = rest[0] && !rest[0].startsWith("--") ? rest[0] : ctx.environmentId;
621
793
  const name = flags.name;
622
794
  const description = flags.description;
623
795
  if (!name && description === undefined) {
624
796
  console.error("Provide at least --name or --description to update.");
625
- process.exit(1);
797
+ return await exitCli(1);
626
798
  }
627
799
  try {
628
- const result = await platform.updateEnvironment(flags.project, envId, name, description);
800
+ const result = await platform.updateEnvironment(ctx.projectId, envId, name, description);
629
801
  console.log("Environment updated:");
630
802
  console.log(JSON.stringify(result, null, 2));
631
803
  }
@@ -643,174 +815,234 @@ async function runEnv(action, rest) {
643
815
  catch { /* use raw message */ }
644
816
  }
645
817
  console.error(message);
646
- process.exit(1);
818
+ return await exitCli(1);
647
819
  }
648
- break;
820
+ return;
649
821
  }
650
- case "set-var": {
822
+ default:
823
+ console.error(`Unknown environment command: ${action}`);
824
+ console.error("Usage: ascendkit environment show|update|promote");
825
+ return await exitCli(1);
826
+ }
827
+ }
828
+ async function runKeystore(action, rest) {
829
+ const ctx = loadEnvContext();
830
+ if (!ctx) {
831
+ console.error("No environment set. Run: ascendkit set-env <public-key>");
832
+ return await exitCli(1);
833
+ }
834
+ const current = await platform.getEnvironment(ctx.projectId, ctx.environmentId);
835
+ const vars = { ...(current.variables ?? {}) };
836
+ const systemVars = Array.isArray(current.systemVariables)
837
+ ? current.systemVariables
838
+ : [];
839
+ switch (action) {
840
+ case "set": {
651
841
  const key = rest[0];
652
842
  const value = rest[1];
653
843
  if (!key || value === undefined) {
654
- console.error("Usage: ascendkit env set-var <key> <value>");
655
- process.exit(1);
656
- }
657
- const ctx = loadEnvContext();
658
- if (!ctx) {
659
- console.error("No environment set. Run: ascendkit env use <tier> --project <project-id>");
660
- process.exit(1);
844
+ console.error("Usage: ascendkit keystore set <key> <value>");
845
+ return await exitCli(1);
661
846
  }
662
- const current = await platform.getEnvironment(ctx.projectId, ctx.environmentId);
663
- const vars = { ...(current.variables ?? {}) };
664
847
  vars[key] = value;
665
848
  await platform.updateEnvironmentVariables(ctx.projectId, ctx.environmentId, vars);
666
- console.log(`Set ${key}=${value}`);
667
- break;
849
+ console.log(`Saved ${key}=${value}`);
850
+ return;
668
851
  }
669
- case "unset-var": {
852
+ case "remove": {
670
853
  const key = rest[0];
671
854
  if (!key) {
672
- console.error("Usage: ascendkit env unset-var <key>");
673
- process.exit(1);
855
+ console.error("Usage: ascendkit keystore remove <key>");
856
+ return await exitCli(1);
674
857
  }
675
- const ctx = loadEnvContext();
676
- if (!ctx) {
677
- console.error("No environment set. Run: ascendkit env use <tier> --project <project-id>");
678
- process.exit(1);
679
- }
680
- const current = await platform.getEnvironment(ctx.projectId, ctx.environmentId);
681
- const vars = { ...(current.variables ?? {}) };
682
858
  if (!(key in vars)) {
683
859
  console.error(`Variable "${key}" not found.`);
684
- process.exit(1);
860
+ return await exitCli(1);
685
861
  }
686
862
  delete vars[key];
687
863
  await platform.updateEnvironmentVariables(ctx.projectId, ctx.environmentId, vars);
688
- console.log(`Unset ${key}`);
689
- break;
864
+ console.log(`Removed ${key}`);
865
+ return;
690
866
  }
691
- case "list-vars": {
692
- const ctx = loadEnvContext();
693
- if (!ctx) {
694
- console.error("No environment set. Run: ascendkit env use <tier> --project <project-id>");
695
- process.exit(1);
696
- }
697
- const current = await platform.getEnvironment(ctx.projectId, ctx.environmentId);
698
- const vars = current.variables ?? {};
699
- const entries = Object.entries(vars);
867
+ case "list": {
868
+ const entries = Object.entries(vars).map(([key, value]) => ({ key, value }));
700
869
  if (entries.length === 0) {
701
- console.log("No variables set.");
870
+ console.log("No custom variables set.");
702
871
  }
703
872
  else {
704
- for (const [k, v] of entries) {
705
- console.log(`${k}=${v || "(empty)"}`);
706
- }
873
+ console.log("Custom variables:");
874
+ table(entries, [
875
+ { key: "key", label: "Key", width: 32 },
876
+ { key: "value", label: "Value", width: 60 },
877
+ ]);
707
878
  }
708
- break;
879
+ if (systemVars.length > 0) {
880
+ console.log(entries.length > 0 ? "\nSystem variables:" : "System variables:");
881
+ table(systemVars.map((item) => ({
882
+ key: item.key,
883
+ value: item.valuePreview,
884
+ availability: item.availability,
885
+ })), [
886
+ { key: "key", label: "Key", width: 24 },
887
+ { key: "value", label: "Value", width: 48 },
888
+ { key: "availability", label: "Availability", width: 22 },
889
+ ]);
890
+ }
891
+ return;
709
892
  }
710
893
  default:
711
- console.error(`Unknown env command: ${action}`);
712
- console.error("Usage: ascendkit env list|use|update|promote|set-var|unset-var|list-vars");
713
- process.exit(1);
894
+ console.error(`Unknown keystore command: ${action}`);
895
+ console.error("Usage: ascendkit keystore list|set|remove");
896
+ return await exitCli(1);
714
897
  }
715
898
  }
716
899
  async function runAuth(client, action, rest) {
717
900
  const flags = parseFlags(rest);
718
- switch (action) {
719
- case "settings":
720
- if (rest[0] === "update") {
721
- const params = {};
722
- if (flags.providers)
723
- params.providers = flags.providers.split(",");
724
- if (flags["email-verification"] || flags.waitlist || flags["password-reset"]) {
725
- params.features = {};
726
- if (flags["email-verification"])
727
- params.features.emailVerification = flags["email-verification"] === "true";
728
- if (flags.waitlist)
729
- params.features.waitlist = flags.waitlist === "true";
730
- if (flags["password-reset"])
731
- params.features.passwordReset = flags["password-reset"] === "true";
901
+ const normalizedAction = !action ? "show" :
902
+ action === "settings" ? (rest[0] === "update" ? "update" : "show") :
903
+ action === "show" ? "show" :
904
+ action === "update" ? "update" :
905
+ action;
906
+ switch (normalizedAction) {
907
+ case "show":
908
+ printAuthSettingsSummary(await auth.getSettings(client));
909
+ break;
910
+ case "update": {
911
+ const params = {};
912
+ if (flags.providers)
913
+ params.providers = flags.providers.split(",");
914
+ if (flags["email-verification"] || flags.waitlist || flags["password-reset"]) {
915
+ params.features = {};
916
+ if (flags["email-verification"])
917
+ params.features.emailVerification = flags["email-verification"] === "true";
918
+ if (flags.waitlist)
919
+ params.features.waitlist = flags.waitlist === "true";
920
+ if (flags["password-reset"])
921
+ params.features.passwordReset = flags["password-reset"] === "true";
922
+ }
923
+ if (flags["session-duration"])
924
+ params.sessionDuration = flags["session-duration"];
925
+ printAuthSettingsSummary(await auth.updateSettings(client, params));
926
+ break;
927
+ }
928
+ case "provider":
929
+ case "providers":
930
+ if (normalizedAction === "providers" || rest[0] === "set") {
931
+ const providersArg = normalizedAction === "providers" ? rest[0] : rest[1];
932
+ if (!providersArg) {
933
+ console.error("Usage: ascendkit auth provider set <p1,p2,...>");
934
+ return await exitCli(1);
732
935
  }
733
- if (flags["session-duration"])
734
- params.sessionDuration = flags["session-duration"];
735
- const s = await auth.updateSettings(client, params);
736
- console.log(`Providers: ${Array.isArray(s.providers) ? s.providers.join(", ") : "none"}`);
737
- const f = s.features ?? {};
738
- const enabled = Object.entries(f).filter(([, v]) => v).map(([k]) => k);
739
- if (enabled.length)
740
- console.log(`Features: ${enabled.join(", ")}`);
741
- if (s.sessionDuration)
742
- console.log(`Session: ${s.sessionDuration}`);
936
+ printAuthSettingsSummary(await auth.updateProviders(client, providersArg.split(",")));
743
937
  }
744
938
  else {
745
- output(await auth.getSettings(client));
939
+ const settings = await auth.getSettings(client);
940
+ const providers = Array.isArray(settings.providers) ? settings.providers.map((provider) => ({ provider })) : [];
941
+ if (providers.length === 0)
942
+ console.log("No providers configured.");
943
+ else
944
+ table(providers, [{ key: "provider", label: "Provider", width: 20 }]);
746
945
  }
747
946
  break;
748
- case "providers":
749
- if (!rest[0]) {
750
- console.error("Usage: ascendkit auth providers <p1,p2,...>");
751
- process.exit(1);
752
- }
753
- output(await auth.updateProviders(client, rest[0].split(",")));
754
- break;
755
947
  case "oauth": {
756
- if (rest[0] === "set") {
757
- const provider = rest[1];
948
+ const oauthAction = rest[0] === "open" || rest[0] === "set" || rest[0] === "remove" ? rest[0] : "open";
949
+ const provider = oauthAction === "open" && rest[0] !== "open" ? rest[0] : rest[1];
950
+ if (oauthAction === "set") {
758
951
  if (!provider || !flags["client-id"]) {
759
952
  console.error("Usage: ascendkit auth oauth set <provider> --client-id <id> [--client-secret <secret> | --client-secret-stdin] [--callback-url <url>]");
760
- process.exit(1);
953
+ return await exitCli(1);
761
954
  }
762
955
  const secretFromArg = flags["client-secret"];
763
956
  const secretFromStdin = flags["client-secret-stdin"] === "true";
764
957
  if (!secretFromArg && !secretFromStdin) {
765
958
  console.error("Missing client secret. Use --client-secret-stdin (recommended) or --client-secret.");
766
- process.exit(1);
959
+ return await exitCli(1);
767
960
  }
768
961
  if (secretFromArg && secretFromStdin) {
769
962
  console.error("Use only one of --client-secret or --client-secret-stdin.");
770
- process.exit(1);
963
+ return await exitCli(1);
771
964
  }
772
965
  const clientSecret = secretFromArg ?? await readSecretFromStdin();
773
966
  if (!clientSecret) {
774
967
  console.error("Client secret cannot be empty.");
775
- process.exit(1);
968
+ return await exitCli(1);
969
+ }
970
+ await auth.updateOAuthCredentials(client, provider, flags["client-id"], clientSecret, flags["callback-url"]);
971
+ console.log(`Saved OAuth credentials for ${provider}.`);
972
+ }
973
+ else if (oauthAction === "remove") {
974
+ if (!provider) {
975
+ console.error("Usage: ascendkit auth oauth remove <provider>");
976
+ return await exitCli(1);
776
977
  }
777
- output(await auth.updateOAuthCredentials(client, provider, flags["client-id"], clientSecret, flags["callback-url"]));
978
+ await auth.deleteOAuthCredentials(client, provider);
979
+ console.log(`Removed OAuth credentials for ${provider}.`);
778
980
  }
779
981
  else {
780
- if (!rest[0]) {
781
- console.error("Usage: ascendkit auth oauth <provider>");
782
- process.exit(1);
982
+ if (!provider) {
983
+ console.error("Usage: ascendkit auth oauth open <provider>");
984
+ return await exitCli(1);
783
985
  }
784
986
  const portalUrl = process.env.ASCENDKIT_PORTAL_URL ?? "http://localhost:3000";
785
- const url = auth.getOAuthSetupUrl(portalUrl, rest[0], client.currentPublicKey ?? undefined);
786
- console.log(`Opening browser to configure ${rest[0]} OAuth credentials...`);
987
+ const url = auth.getOAuthSetupUrl(portalUrl, provider, client.currentPublicKey ?? undefined);
988
+ console.log(`Opening browser to configure ${provider} OAuth credentials...`);
787
989
  console.log(url);
788
990
  openBrowser(url);
789
991
  }
790
992
  break;
791
993
  }
994
+ case "user":
792
995
  case "users":
793
- table(await auth.listUsers(client), [
794
- { key: "id", label: "ID" },
795
- { key: "email", label: "Email", width: 35 },
796
- { key: "name", label: "Name", width: 25 },
797
- { key: "status", label: "Status" },
798
- ]);
996
+ if (normalizedAction === "users" || !rest[0] || rest[0] === "list") {
997
+ table(await auth.listUsers(client), [
998
+ { key: "id", label: "ID" },
999
+ { key: "email", label: "Email", width: 35 },
1000
+ { key: "name", label: "Name", width: 25 },
1001
+ { key: "status", label: "Status" },
1002
+ ]);
1003
+ }
1004
+ else if (rest[0] === "remove") {
1005
+ if (!rest[1]) {
1006
+ console.error("Usage: ascendkit auth user remove <user-id>");
1007
+ return await exitCli(1);
1008
+ }
1009
+ await auth.deleteUser(client, rest[1]);
1010
+ console.log(`Removed user ${rest[1]}.`);
1011
+ }
1012
+ else if (rest[0] === "reactivate") {
1013
+ if (!rest[1]) {
1014
+ console.error("Usage: ascendkit auth user reactivate <user-id>");
1015
+ return await exitCli(1);
1016
+ }
1017
+ await auth.reactivateUser(client, rest[1]);
1018
+ console.log(`Reactivated user ${rest[1]}.`);
1019
+ }
1020
+ else {
1021
+ console.error(`Unknown auth user command: ${rest[0]}`);
1022
+ return await exitCli(1);
1023
+ }
799
1024
  break;
800
1025
  default:
801
1026
  console.error(`Unknown auth command: ${action}`);
802
- process.exit(1);
1027
+ return await exitCli(1);
803
1028
  }
804
1029
  }
805
1030
  async function runContent(client, action, rest) {
806
1031
  const flags = parseFlags(rest);
807
- switch (action) {
1032
+ if (!action) {
1033
+ console.log(HELP_SECTION.template);
1034
+ return;
1035
+ }
1036
+ const normalizedAction = action === "show" || action === "get" ? "show" :
1037
+ action === "remove" || action === "delete" ? "remove" :
1038
+ action;
1039
+ switch (normalizedAction) {
808
1040
  case "create":
809
1041
  if (!flags.name || !flags.subject || !flags["body-html"] || !flags["body-text"]) {
810
- console.error("Usage: ascendkit templates create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>]");
811
- process.exit(1);
1042
+ console.error("Usage: ascendkit template create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>]");
1043
+ return await exitCli(1);
812
1044
  }
813
- output(await content.createTemplate(client, {
1045
+ printTemplateSummary(await content.createTemplate(client, {
814
1046
  name: flags.name, subject: flags.subject,
815
1047
  bodyHtml: flags["body-html"], bodyText: flags["body-text"],
816
1048
  slug: flags.slug, description: flags.description,
@@ -833,64 +1065,79 @@ async function runContent(client, action, rest) {
833
1065
  ]);
834
1066
  break;
835
1067
  }
836
- case "get":
1068
+ case "show":
837
1069
  if (!rest[0]) {
838
- console.error("Usage: ascendkit templates get <template-id>");
839
- process.exit(1);
1070
+ console.error("Usage: ascendkit template show <template-id>");
1071
+ return await exitCli(1);
840
1072
  }
841
- output(await content.getTemplate(client, rest[0]));
1073
+ printTemplateSummary(await content.getTemplate(client, rest[0]), { verbose: true });
842
1074
  break;
843
1075
  case "update":
844
1076
  if (!rest[0]) {
845
- console.error("Usage: ascendkit templates update <template-id> [--flags]");
846
- process.exit(1);
1077
+ console.error("Usage: ascendkit template update <template-id> [--flags]");
1078
+ return await exitCli(1);
847
1079
  }
848
- output(await content.updateTemplate(client, rest[0], {
1080
+ printTemplateSummary(await content.updateTemplate(client, rest[0], {
849
1081
  subject: flags.subject,
850
1082
  bodyHtml: flags["body-html"],
851
1083
  bodyText: flags["body-text"],
852
1084
  changeNote: flags["change-note"],
853
1085
  }));
854
1086
  break;
855
- case "delete":
1087
+ case "remove":
856
1088
  if (!rest[0]) {
857
- console.error("Usage: ascendkit templates delete <template-id>");
858
- process.exit(1);
1089
+ console.error("Usage: ascendkit template remove <template-id>");
1090
+ return await exitCli(1);
859
1091
  }
860
- output(await content.deleteTemplate(client, rest[0]));
1092
+ await content.deleteTemplate(client, rest[0]);
1093
+ console.log(`Removed template ${rest[0]}.`);
861
1094
  break;
862
1095
  case "versions":
863
- if (!rest[0]) {
864
- console.error("Usage: ascendkit templates versions <template-id>");
865
- process.exit(1);
866
- }
867
- output(await content.listVersions(client, rest[0]));
868
- break;
869
1096
  case "version":
870
- if (!rest[0] || !rest[1]) {
871
- console.error("Usage: ascendkit templates version <template-id> <n>");
872
- process.exit(1);
1097
+ if (normalizedAction === "versions" || rest[0] === "list") {
1098
+ const templateId = normalizedAction === "versions" ? rest[0] : rest[1];
1099
+ if (!templateId) {
1100
+ console.error("Usage: ascendkit template version list <template-id>");
1101
+ return await exitCli(1);
1102
+ }
1103
+ const versions = await content.listVersions(client, templateId);
1104
+ table(versions, [
1105
+ { key: "versionNumber", label: "Version" },
1106
+ { key: "createdAt", label: "Created", width: 24 },
1107
+ { key: "changeNote", label: "Change note", width: 40 },
1108
+ ]);
1109
+ }
1110
+ else {
1111
+ const templateId = normalizedAction === "version" && rest[0] === "show" ? rest[1] : normalizedAction === "version" ? rest[0] : rest[1];
1112
+ const versionNumber = normalizedAction === "version" && rest[0] === "show" ? rest[2] : normalizedAction === "version" ? rest[1] : rest[2];
1113
+ if (!templateId || !versionNumber) {
1114
+ console.error("Usage: ascendkit template version show <template-id> <n>");
1115
+ return await exitCli(1);
1116
+ }
1117
+ output(await content.getVersion(client, templateId, parseInt(versionNumber, 10)));
873
1118
  }
874
- output(await content.getVersion(client, rest[0], parseInt(rest[1], 10)));
875
1119
  break;
876
1120
  default:
877
- console.error(`Unknown templates command: ${action}`);
878
- process.exit(1);
1121
+ console.error(`Unknown template command: ${action}`);
1122
+ return await exitCli(1);
879
1123
  }
880
1124
  }
881
1125
  async function runSurvey(client, action, rest) {
882
1126
  const flags = parseFlags(rest);
883
- if (!action) {
1127
+ const normalizedAction = action === "show" || action === "get" ? "show" :
1128
+ action === "remove" || action === "delete" ? "remove" :
1129
+ action;
1130
+ if (!normalizedAction) {
884
1131
  console.log(HELP_SECTION.survey);
885
1132
  return;
886
1133
  }
887
- switch (action) {
1134
+ switch (normalizedAction) {
888
1135
  case "create":
889
1136
  if (!flags.name) {
890
1137
  console.error("Usage: ascendkit survey create --name <n> [--type nps|csat|custom]");
891
- process.exit(1);
1138
+ return await exitCli(1);
892
1139
  }
893
- output(await surveys.createSurvey(client, {
1140
+ printSurveySummary(await surveys.createSurvey(client, {
894
1141
  name: flags.name,
895
1142
  type: flags.type ?? "custom",
896
1143
  definition: flags.definition ? JSON.parse(flags.definition) : undefined,
@@ -904,146 +1151,180 @@ async function runSurvey(client, action, rest) {
904
1151
  { key: "status", label: "Status" },
905
1152
  ]);
906
1153
  break;
907
- case "get":
1154
+ case "show":
908
1155
  if (!rest[0]) {
909
- console.error("Usage: ascendkit survey get <survey-id>");
910
- process.exit(1);
1156
+ console.error("Usage: ascendkit survey show <survey-id>");
1157
+ return await exitCli(1);
911
1158
  }
912
- output(await surveys.getSurvey(client, rest[0]));
1159
+ printSurveySummary(await surveys.getSurvey(client, rest[0]));
913
1160
  break;
914
1161
  case "update":
915
1162
  if (!rest[0]) {
916
1163
  console.error("Usage: ascendkit survey update <survey-id> [--flags]");
917
- process.exit(1);
1164
+ return await exitCli(1);
918
1165
  }
919
- output(await surveys.updateSurvey(client, rest[0], {
1166
+ printSurveySummary(await surveys.updateSurvey(client, rest[0], {
920
1167
  name: flags.name,
921
1168
  status: flags.status,
922
1169
  definition: flags.definition ? JSON.parse(flags.definition) : undefined,
923
1170
  }));
924
1171
  break;
925
- case "delete":
1172
+ case "remove":
926
1173
  if (!rest[0]) {
927
- console.error("Usage: ascendkit survey delete <survey-id>");
928
- process.exit(1);
1174
+ console.error("Usage: ascendkit survey remove <survey-id>");
1175
+ return await exitCli(1);
929
1176
  }
930
- output(await surveys.deleteSurvey(client, rest[0]));
1177
+ await surveys.deleteSurvey(client, rest[0]);
1178
+ console.log(`Removed survey ${rest[0]}.`);
931
1179
  break;
932
1180
  case "distribute":
933
1181
  if (!rest[0] || !flags.users) {
934
1182
  console.error("Usage: ascendkit survey distribute <survey-id> --users <usr_id1,usr_id2,...>");
935
- process.exit(1);
1183
+ return await exitCli(1);
936
1184
  }
937
1185
  output(await surveys.distributeSurvey(client, rest[0], flags.users.split(",")));
938
1186
  break;
939
1187
  case "invitations":
940
- if (!rest[0]) {
941
- console.error("Usage: ascendkit survey invitations <survey-id>");
942
- process.exit(1);
1188
+ case "invitation":
1189
+ if (normalizedAction === "invitations" || rest[0] === "list") {
1190
+ const surveyId = normalizedAction === "invitations" ? rest[0] : rest[1];
1191
+ if (!surveyId) {
1192
+ console.error("Usage: ascendkit survey invitation list <survey-id>");
1193
+ return await exitCli(1);
1194
+ }
1195
+ output(await surveys.listInvitations(client, surveyId));
1196
+ }
1197
+ else {
1198
+ console.error(`Unknown survey invitation command: ${rest[0]}`);
1199
+ return await exitCli(1);
943
1200
  }
944
- output(await surveys.listInvitations(client, rest[0]));
945
1201
  break;
946
1202
  case "analytics":
947
1203
  if (!rest[0]) {
948
1204
  console.error("Usage: ascendkit survey analytics <survey-id>");
949
- process.exit(1);
1205
+ return await exitCli(1);
950
1206
  }
951
1207
  output(await surveys.getAnalytics(client, rest[0]));
952
1208
  break;
953
- case "export-definition": {
954
- if (!rest[0]) {
955
- console.error("Usage: ascendkit survey export-definition <survey-id> [--out <file>]");
956
- process.exit(1);
957
- }
958
- const survey = await surveys.getSurvey(client, rest[0]);
959
- const text = `${JSON.stringify(survey.definition ?? {}, null, 2)}\n`;
960
- const outPath = flags.out || flags.file;
961
- if (outPath) {
962
- writeFileSync(outPath, text, "utf8");
963
- console.log(`Wrote survey definition to ${outPath}`);
1209
+ case "export-definition":
1210
+ case "import-definition":
1211
+ case "definition": {
1212
+ const definitionAction = normalizedAction === "definition" ? rest[0] :
1213
+ normalizedAction === "export-definition" ? "export" :
1214
+ "import";
1215
+ const surveyId = normalizedAction === "definition" ? rest[1] : rest[0];
1216
+ if (definitionAction === "export") {
1217
+ if (!surveyId) {
1218
+ console.error("Usage: ascendkit survey definition export <survey-id> [--out <file>]");
1219
+ return await exitCli(1);
1220
+ }
1221
+ const survey = await surveys.getSurvey(client, surveyId);
1222
+ const text = `${JSON.stringify(survey.definition ?? {}, null, 2)}\n`;
1223
+ const outPath = flags.out || flags.file;
1224
+ if (outPath) {
1225
+ writeFileSync(outPath, text, "utf8");
1226
+ console.log(`Wrote survey definition to ${outPath}`);
1227
+ }
1228
+ else {
1229
+ process.stdout.write(text);
1230
+ }
964
1231
  }
965
1232
  else {
966
- process.stdout.write(text);
1233
+ if (!surveyId || !(flags.in || flags.file)) {
1234
+ console.error("Usage: ascendkit survey definition import <survey-id> --in <file>");
1235
+ return await exitCli(1);
1236
+ }
1237
+ const inPath = flags.in || flags.file;
1238
+ const raw = readFileSync(inPath, "utf8");
1239
+ const parsed = JSON.parse(raw);
1240
+ const candidate = parsed && typeof parsed === "object" && "definition" in parsed
1241
+ ? parsed.definition
1242
+ : parsed;
1243
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
1244
+ console.error("Invalid definition JSON: expected an object or { definition: object }");
1245
+ return await exitCli(1);
1246
+ }
1247
+ printSurveySummary(await surveys.updateSurvey(client, surveyId, {
1248
+ definition: candidate,
1249
+ }));
967
1250
  }
968
1251
  break;
969
1252
  }
970
- case "import-definition": {
971
- if (!rest[0] || !(flags.in || flags.file)) {
972
- console.error("Usage: ascendkit survey import-definition <survey-id> --in <file>");
973
- process.exit(1);
974
- }
975
- const inPath = flags.in || flags.file;
976
- const raw = readFileSync(inPath, "utf8");
977
- const parsed = JSON.parse(raw);
978
- const candidate = parsed && typeof parsed === "object" && "definition" in parsed
979
- ? parsed.definition
980
- : parsed;
981
- if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
982
- console.error("Invalid definition JSON: expected an object or { definition: object }");
983
- process.exit(1);
984
- }
985
- output(await surveys.updateSurvey(client, rest[0], {
986
- definition: candidate,
987
- }));
988
- break;
989
- }
990
1253
  case "list-questions":
991
- if (!rest[0]) {
992
- console.error("Usage: ascendkit survey list-questions <survey-id>");
993
- process.exit(1);
1254
+ case "add-question":
1255
+ case "edit-question":
1256
+ case "remove-question":
1257
+ case "reorder-questions":
1258
+ case "question": {
1259
+ const questionAction = normalizedAction === "question" ? rest[0] :
1260
+ normalizedAction === "list-questions" ? "list" :
1261
+ normalizedAction === "add-question" ? "add" :
1262
+ normalizedAction === "edit-question" ? "update" :
1263
+ normalizedAction === "remove-question" ? "remove" :
1264
+ "reorder";
1265
+ const surveyId = normalizedAction === "question" ? rest[1] : rest[0];
1266
+ const questionName = normalizedAction === "question" ? rest[2] : rest[1];
1267
+ if (!surveyId) {
1268
+ console.error("Usage: ascendkit survey question list <survey-id>");
1269
+ return await exitCli(1);
1270
+ }
1271
+ if (questionAction === "list") {
1272
+ output(await surveys.listQuestions(client, surveyId));
1273
+ }
1274
+ else if (questionAction === "add") {
1275
+ if (!flags.type || !flags.title) {
1276
+ console.error("Usage: ascendkit survey question add <survey-id> --type <type> --title <title> [--name <name>] [--required <true|false>] [--choices <c1,c2,...>] [--position <n>]");
1277
+ return await exitCli(1);
1278
+ }
1279
+ const params = { type: flags.type, title: flags.title };
1280
+ if (flags.name)
1281
+ params.name = flags.name;
1282
+ if (flags.required)
1283
+ params.isRequired = flags.required === "true";
1284
+ if (flags.choices)
1285
+ params.choices = flags.choices.split(",");
1286
+ if (flags.position != null)
1287
+ params.position = Number(flags.position);
1288
+ output(await surveys.addQuestion(client, surveyId, params));
1289
+ }
1290
+ else if (questionAction === "update") {
1291
+ if (!questionName) {
1292
+ console.error("Usage: ascendkit survey question update <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]");
1293
+ return await exitCli(1);
1294
+ }
1295
+ const params = {};
1296
+ if (flags.title)
1297
+ params.title = flags.title;
1298
+ if (flags.required)
1299
+ params.isRequired = flags.required === "true";
1300
+ if (flags.choices)
1301
+ params.choices = flags.choices.split(",");
1302
+ output(await surveys.editQuestion(client, surveyId, questionName, params));
1303
+ }
1304
+ else if (questionAction === "remove") {
1305
+ if (!questionName) {
1306
+ console.error("Usage: ascendkit survey question remove <survey-id> <question-name>");
1307
+ return await exitCli(1);
1308
+ }
1309
+ output(await surveys.removeQuestion(client, surveyId, questionName));
994
1310
  }
995
- output(await surveys.listQuestions(client, rest[0]));
996
- break;
997
- case "add-question": {
998
- if (!rest[0] || !flags.type || !flags.title) {
999
- console.error("Usage: ascendkit survey add-question <survey-id> --type <type> --title <title> [--name <name>] [--required <true|false>] [--choices <c1,c2,...>] [--position <n>]");
1000
- process.exit(1);
1311
+ else if (questionAction === "reorder") {
1312
+ if (!flags.order) {
1313
+ console.error("Usage: ascendkit survey question reorder <survey-id> --order <name1,name2,...>");
1314
+ return await exitCli(1);
1315
+ }
1316
+ output(await surveys.reorderQuestions(client, surveyId, flags.order.split(",")));
1001
1317
  }
1002
- const params = { type: flags.type, title: flags.title };
1003
- if (flags.name)
1004
- params.name = flags.name;
1005
- if (flags.required)
1006
- params.isRequired = flags.required === "true";
1007
- if (flags.choices)
1008
- params.choices = flags.choices.split(",");
1009
- if (flags.position != null)
1010
- params.position = Number(flags.position);
1011
- output(await surveys.addQuestion(client, rest[0], params));
1012
- break;
1013
- }
1014
- case "edit-question": {
1015
- if (!rest[0] || !rest[1]) {
1016
- console.error("Usage: ascendkit survey edit-question <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]");
1017
- process.exit(1);
1318
+ else {
1319
+ console.error(`Unknown survey question command: ${questionAction}`);
1320
+ return await exitCli(1);
1018
1321
  }
1019
- const params = {};
1020
- if (flags.title)
1021
- params.title = flags.title;
1022
- if (flags.required)
1023
- params.isRequired = flags.required === "true";
1024
- if (flags.choices)
1025
- params.choices = flags.choices.split(",");
1026
- output(await surveys.editQuestion(client, rest[0], rest[1], params));
1027
1322
  break;
1028
1323
  }
1029
- case "remove-question":
1030
- if (!rest[0] || !rest[1]) {
1031
- console.error("Usage: ascendkit survey remove-question <survey-id> <question-name>");
1032
- process.exit(1);
1033
- }
1034
- output(await surveys.removeQuestion(client, rest[0], rest[1]));
1035
- break;
1036
- case "reorder-questions":
1037
- if (!rest[0] || !flags.order) {
1038
- console.error("Usage: ascendkit survey reorder-questions <survey-id> --order <name1,name2,...>");
1039
- process.exit(1);
1040
- }
1041
- output(await surveys.reorderQuestions(client, rest[0], flags.order.split(",")));
1042
- break;
1043
1324
  default:
1044
1325
  console.error(`Unknown survey command: ${action}`);
1045
1326
  console.error('Run "ascendkit survey --help" for usage');
1046
- process.exit(1);
1327
+ return await exitCli(1);
1047
1328
  }
1048
1329
  }
1049
1330
  function runStatus() {
@@ -1064,7 +1345,7 @@ function runStatus() {
1064
1345
  }
1065
1346
  else {
1066
1347
  console.log(" No environment set. Run: ascendkit set-env <public-key>");
1067
- console.log(" List environments: ascendkit env list --project <project-id>");
1348
+ console.log(" List environments: ascendkit project env list <project-id>");
1068
1349
  }
1069
1350
  console.log();
1070
1351
  }
@@ -1128,14 +1409,42 @@ async function runVerify() {
1128
1409
  console.log();
1129
1410
  }
1130
1411
  async function runJourney(client, action, rest) {
1412
+ if (!action) {
1413
+ console.log(HELP_SECTION.journey);
1414
+ return;
1415
+ }
1416
+ if (action === "show")
1417
+ action = "get";
1418
+ if (action === "remove")
1419
+ action = "delete";
1420
+ if (action === "node") {
1421
+ const nodeAction = rest[0];
1422
+ action =
1423
+ nodeAction === "list" ? "list-nodes" :
1424
+ nodeAction === "add" ? "add-node" :
1425
+ nodeAction === "update" ? "edit-node" :
1426
+ nodeAction === "remove" ? "remove-node" :
1427
+ action;
1428
+ rest = rest.slice(1);
1429
+ }
1430
+ if (action === "transition") {
1431
+ const transitionAction = rest[0];
1432
+ action =
1433
+ transitionAction === "list" ? "list-transitions" :
1434
+ transitionAction === "add" ? "add-transition" :
1435
+ transitionAction === "update" ? "edit-transition" :
1436
+ transitionAction === "remove" ? "remove-transition" :
1437
+ action;
1438
+ rest = rest.slice(1);
1439
+ }
1131
1440
  const flags = parseFlags(rest);
1132
1441
  switch (action) {
1133
1442
  case "create":
1134
1443
  if (!flags.name || !flags["entry-event"] || !flags["entry-node"]) {
1135
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>]");
1136
- process.exit(1);
1445
+ return await exitCli(1);
1137
1446
  }
1138
- output(await journeys.createJourney(client, {
1447
+ console.log(formatJourneyWithGuidance((await journeys.createJourney(client, {
1139
1448
  name: flags.name,
1140
1449
  entryEvent: flags["entry-event"],
1141
1450
  entryNode: flags["entry-node"],
@@ -1144,7 +1453,7 @@ async function runJourney(client, action, rest) {
1144
1453
  description: flags.description,
1145
1454
  entryConditions: flags["entry-conditions"] ? JSON.parse(flags["entry-conditions"]) : undefined,
1146
1455
  reEntryPolicy: flags["re-entry-policy"],
1147
- }));
1456
+ }))));
1148
1457
  break;
1149
1458
  case "list": {
1150
1459
  const opts = {};
@@ -1162,17 +1471,17 @@ async function runJourney(client, action, rest) {
1162
1471
  }
1163
1472
  case "get":
1164
1473
  if (!rest[0]) {
1165
- console.error("Usage: ascendkit journey get <journey-id>");
1166
- process.exit(1);
1474
+ console.error("Usage: ascendkit journey show <journey-id>");
1475
+ return await exitCli(1);
1167
1476
  }
1168
- output(await journeys.getJourney(client, rest[0]));
1477
+ console.log(formatJourneyWithGuidance(await journeys.getJourney(client, rest[0])));
1169
1478
  break;
1170
1479
  case "update":
1171
1480
  if (!rest[0]) {
1172
1481
  console.error("Usage: ascendkit journey update <journey-id> [--flags]");
1173
- process.exit(1);
1482
+ return await exitCli(1);
1174
1483
  }
1175
- output(await journeys.updateJourney(client, rest[0], {
1484
+ console.log(formatJourneyWithGuidance((await journeys.updateJourney(client, rest[0], {
1176
1485
  name: flags.name,
1177
1486
  description: flags.description,
1178
1487
  entryEvent: flags["entry-event"],
@@ -1181,54 +1490,62 @@ async function runJourney(client, action, rest) {
1181
1490
  reEntryPolicy: flags["re-entry-policy"],
1182
1491
  nodes: flags.nodes ? JSON.parse(flags.nodes) : undefined,
1183
1492
  transitions: flags.transitions ? JSON.parse(flags.transitions) : undefined,
1184
- }));
1493
+ }))));
1185
1494
  break;
1186
1495
  case "delete":
1187
1496
  if (!rest[0]) {
1188
- console.error("Usage: ascendkit journey delete <journey-id>");
1189
- process.exit(1);
1497
+ console.error("Usage: ascendkit journey remove <journey-id>");
1498
+ return await exitCli(1);
1190
1499
  }
1191
- output(await journeys.deleteJourney(client, rest[0]));
1500
+ await journeys.deleteJourney(client, rest[0]);
1501
+ console.log(`Deleted journey ${rest[0]}.`);
1192
1502
  break;
1193
1503
  case "activate":
1194
1504
  if (!rest[0]) {
1195
1505
  console.error("Usage: ascendkit journey activate <journey-id>");
1196
- process.exit(1);
1506
+ return await exitCli(1);
1197
1507
  }
1198
- output(await journeys.activateJourney(client, rest[0]));
1508
+ console.log(formatJourneyWithGuidance(await journeys.activateJourney(client, rest[0])));
1199
1509
  break;
1200
1510
  case "pause":
1201
1511
  if (!rest[0]) {
1202
1512
  console.error("Usage: ascendkit journey pause <journey-id>");
1203
- process.exit(1);
1513
+ return await exitCli(1);
1204
1514
  }
1205
- output(await journeys.pauseJourney(client, rest[0]));
1515
+ console.log(formatJourneyWithGuidance(await journeys.pauseJourney(client, rest[0])));
1516
+ break;
1517
+ case "resume":
1518
+ if (!rest[0]) {
1519
+ console.error("Usage: ascendkit journey resume <journey-id>");
1520
+ return await exitCli(1);
1521
+ }
1522
+ console.log(formatJourneyWithGuidance(await journeys.resumeJourney(client, rest[0])));
1206
1523
  break;
1207
1524
  case "archive":
1208
1525
  if (!rest[0]) {
1209
1526
  console.error("Usage: ascendkit journey archive <journey-id>");
1210
- process.exit(1);
1527
+ return await exitCli(1);
1211
1528
  }
1212
- output(await journeys.archiveJourney(client, rest[0]));
1529
+ console.log(formatJourneyWithGuidance(await journeys.archiveJourney(client, rest[0])));
1213
1530
  break;
1214
1531
  case "analytics":
1215
1532
  if (!rest[0]) {
1216
1533
  console.error("Usage: ascendkit journey analytics <journey-id>");
1217
- process.exit(1);
1534
+ return await exitCli(1);
1218
1535
  }
1219
- output(await journeys.getJourneyAnalytics(client, rest[0]));
1536
+ console.log(formatJourneyAnalytics(await journeys.getJourneyAnalytics(client, rest[0])));
1220
1537
  break;
1221
1538
  case "list-nodes":
1222
1539
  if (!rest[0]) {
1223
- console.error("Usage: ascendkit journey list-nodes <journey-id>");
1224
- process.exit(1);
1540
+ console.error("Usage: ascendkit journey node list <journey-id>");
1541
+ return await exitCli(1);
1225
1542
  }
1226
- output(await journeys.listNodes(client, rest[0]));
1543
+ console.log(formatNodeList(await journeys.listNodes(client, rest[0])));
1227
1544
  break;
1228
1545
  case "add-node": {
1229
1546
  if (!rest[0] || !flags.name) {
1230
- console.error("Usage: ascendkit journey add-node <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
1231
- process.exit(1);
1547
+ console.error("Usage: ascendkit journey node add <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
1548
+ return await exitCli(1);
1232
1549
  }
1233
1550
  const params = { name: flags.name };
1234
1551
  if (flags.action)
@@ -1236,19 +1553,19 @@ async function runJourney(client, action, rest) {
1236
1553
  if (flags["email-id"]) {
1237
1554
  if (!params.action || params.action.type !== "send_email") {
1238
1555
  console.error("--email-id requires a send_email action (use --action '{\"type\": \"send_email\", \"templateSlug\": \"...\"}')");
1239
- process.exit(1);
1556
+ return await exitCli(1);
1240
1557
  }
1241
1558
  params.action.fromIdentityEmail = flags["email-id"];
1242
1559
  }
1243
1560
  if (flags.terminal)
1244
1561
  params.terminal = flags.terminal === "true";
1245
- output(await journeys.addNode(client, rest[0], params));
1562
+ console.log(formatSingleNode(await journeys.addNode(client, rest[0], params), "Added", flags.name));
1246
1563
  break;
1247
1564
  }
1248
1565
  case "edit-node": {
1249
1566
  if (!rest[0] || !rest[1]) {
1250
- console.error("Usage: ascendkit journey edit-node <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
1251
- process.exit(1);
1567
+ console.error("Usage: ascendkit journey node update <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
1568
+ return await exitCli(1);
1252
1569
  }
1253
1570
  const params = {};
1254
1571
  if (flags.action)
@@ -1259,43 +1576,43 @@ async function runJourney(client, action, rest) {
1259
1576
  const current = nodesData.nodes?.find((n) => n.name === rest[1]);
1260
1577
  if (!current?.action || current.action.type !== "send_email") {
1261
1578
  console.error("--email-id can only be set on send_email nodes");
1262
- process.exit(1);
1579
+ return await exitCli(1);
1263
1580
  }
1264
1581
  params.action = current.action;
1265
1582
  }
1266
1583
  else if (params.action.type !== "send_email") {
1267
1584
  console.error("--email-id requires a send_email action");
1268
- process.exit(1);
1585
+ return await exitCli(1);
1269
1586
  }
1270
1587
  params.action.fromIdentityEmail = flags["email-id"];
1271
1588
  }
1272
1589
  if (flags.terminal)
1273
1590
  params.terminal = flags.terminal === "true";
1274
- output(await journeys.editNode(client, rest[0], rest[1], params));
1591
+ console.log(formatSingleNode(await journeys.editNode(client, rest[0], rest[1], params), "Updated", rest[1]));
1275
1592
  break;
1276
1593
  }
1277
1594
  case "remove-node":
1278
1595
  if (!rest[0] || !rest[1]) {
1279
- console.error("Usage: ascendkit journey remove-node <journey-id> <node-name>");
1280
- process.exit(1);
1596
+ console.error("Usage: ascendkit journey node remove <journey-id> <node-name>");
1597
+ return await exitCli(1);
1281
1598
  }
1282
- output(await journeys.removeNode(client, rest[0], rest[1]));
1599
+ console.log(formatSingleNode(await journeys.removeNode(client, rest[0], rest[1]), "Removed", rest[1]));
1283
1600
  break;
1284
1601
  case "list-transitions": {
1285
1602
  if (!rest[0]) {
1286
- console.error("Usage: ascendkit journey list-transitions <journey-id> [--from <node-name>] [--to <node-name>]");
1287
- process.exit(1);
1603
+ console.error("Usage: ascendkit journey transition list <journey-id> [--from <node-name>] [--to <node-name>]");
1604
+ return await exitCli(1);
1288
1605
  }
1289
- output(await journeys.listTransitions(client, rest[0], {
1606
+ console.log(formatTransitionList(await journeys.listTransitions(client, rest[0], {
1290
1607
  from_node: flags.from,
1291
1608
  to_node: flags.to,
1292
- }));
1609
+ })));
1293
1610
  break;
1294
1611
  }
1295
1612
  case "add-transition": {
1296
1613
  if (!rest[0] || !flags.from || !flags.to || !(flags.on || flags.after || flags.trigger)) {
1297
- console.error("Usage: ascendkit journey add-transition <journey-id> --from <node-name> --to <node-name> --on <event> | --after <delay> | --trigger <json> [--priority <n>] [--name <transition-name>]");
1298
- process.exit(1);
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>]");
1615
+ return await exitCli(1);
1299
1616
  }
1300
1617
  let trigger;
1301
1618
  if (flags.on) {
@@ -1316,13 +1633,14 @@ async function runJourney(client, action, rest) {
1316
1633
  params.priority = Number(flags.priority);
1317
1634
  if (flags.name)
1318
1635
  params.name = flags.name;
1319
- output(await journeys.addTransition(client, rest[0], params));
1636
+ const transitionName = flags.name || `${flags.from}-to-${flags.to}`;
1637
+ console.log(formatSingleTransition(await journeys.addTransition(client, rest[0], params), "Added", transitionName));
1320
1638
  break;
1321
1639
  }
1322
1640
  case "edit-transition": {
1323
1641
  if (!rest[0] || !rest[1]) {
1324
- console.error("Usage: ascendkit journey edit-transition <journey-id> <transition-name> [--on <event>] [--after <delay>] [--trigger <json>] [--priority <n>]");
1325
- process.exit(1);
1642
+ console.error("Usage: ascendkit journey transition update <journey-id> <transition-name> [--on <event>] [--after <delay>] [--trigger <json>] [--priority <n>]");
1643
+ return await exitCli(1);
1326
1644
  }
1327
1645
  const params = {};
1328
1646
  if (flags.on) {
@@ -1336,19 +1654,19 @@ async function runJourney(client, action, rest) {
1336
1654
  }
1337
1655
  if (flags.priority != null)
1338
1656
  params.priority = Number(flags.priority);
1339
- output(await journeys.editTransition(client, rest[0], rest[1], params));
1657
+ console.log(formatSingleTransition(await journeys.editTransition(client, rest[0], rest[1], params), "Updated", rest[1]));
1340
1658
  break;
1341
1659
  }
1342
1660
  case "remove-transition":
1343
1661
  if (!rest[0] || !rest[1]) {
1344
- console.error("Usage: ascendkit journey remove-transition <journey-id> <transition-name>");
1345
- process.exit(1);
1662
+ console.error("Usage: ascendkit journey transition remove <journey-id> <transition-name>");
1663
+ return await exitCli(1);
1346
1664
  }
1347
- output(await journeys.removeTransition(client, rest[0], rest[1]));
1665
+ console.log(formatSingleTransition(await journeys.removeTransition(client, rest[0], rest[1]), "Removed", rest[1]));
1348
1666
  break;
1349
1667
  default:
1350
1668
  console.error(`Unknown journey command: ${action}`);
1351
- process.exit(1);
1669
+ return await exitCli(1);
1352
1670
  }
1353
1671
  }
1354
1672
  async function runWebhook(client, action, rest) {
@@ -1357,7 +1675,7 @@ async function runWebhook(client, action, rest) {
1357
1675
  case "create":
1358
1676
  if (!flags.url) {
1359
1677
  console.error("Usage: ascendkit webhook create --url <url> [--events <e1,e2,...>]");
1360
- process.exit(1);
1678
+ return await exitCli(1);
1361
1679
  }
1362
1680
  output(await webhooks.createWebhook(client, {
1363
1681
  url: flags.url,
@@ -1375,14 +1693,14 @@ async function runWebhook(client, action, rest) {
1375
1693
  case "get":
1376
1694
  if (!rest[0]) {
1377
1695
  console.error("Usage: ascendkit webhook get <webhook-id>");
1378
- process.exit(1);
1696
+ return await exitCli(1);
1379
1697
  }
1380
1698
  output(await webhooks.getWebhook(client, rest[0]));
1381
1699
  break;
1382
1700
  case "update": {
1383
1701
  if (!rest[0]) {
1384
1702
  console.error("Usage: ascendkit webhook update <webhook-id> [--flags]");
1385
- process.exit(1);
1703
+ return await exitCli(1);
1386
1704
  }
1387
1705
  const updated = await webhooks.updateWebhook(client, rest[0], {
1388
1706
  url: flags.url,
@@ -1398,21 +1716,21 @@ async function runWebhook(client, action, rest) {
1398
1716
  case "delete":
1399
1717
  if (!rest[0]) {
1400
1718
  console.error("Usage: ascendkit webhook delete <webhook-id>");
1401
- process.exit(1);
1719
+ return await exitCli(1);
1402
1720
  }
1403
1721
  output(await webhooks.deleteWebhook(client, rest[0]));
1404
1722
  break;
1405
1723
  case "test":
1406
1724
  if (!rest[0]) {
1407
1725
  console.error("Usage: ascendkit webhook test <webhook-id> [--event <event-type>]");
1408
- process.exit(1);
1726
+ return await exitCli(1);
1409
1727
  }
1410
1728
  output(await webhooks.testWebhook(client, rest[0], flags.event));
1411
1729
  break;
1412
1730
  default:
1413
1731
  console.error(`Unknown webhook command: ${action}`);
1414
1732
  console.error('Run "ascendkit webhook --help" for usage');
1415
- process.exit(1);
1733
+ return await exitCli(1);
1416
1734
  }
1417
1735
  }
1418
1736
  async function runCampaign(client, action, rest) {
@@ -1422,10 +1740,10 @@ async function runCampaign(client, action, rest) {
1422
1740
  return;
1423
1741
  }
1424
1742
  switch (action) {
1425
- case "create":
1743
+ case "create": {
1426
1744
  if (!flags.name || !flags.template || !flags.audience) {
1427
1745
  console.error("Usage: ascendkit campaign create --name <name> --template <template-id> --audience <json> [--scheduled-at <datetime>]");
1428
- process.exit(1);
1746
+ return await exitCli(1);
1429
1747
  }
1430
1748
  let createFilter;
1431
1749
  try {
@@ -1433,7 +1751,7 @@ async function runCampaign(client, action, rest) {
1433
1751
  }
1434
1752
  catch {
1435
1753
  console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1436
- process.exit(1);
1754
+ return await exitCli(1);
1437
1755
  }
1438
1756
  output(await campaigns.createCampaign(client, {
1439
1757
  name: flags.name,
@@ -1442,6 +1760,7 @@ async function runCampaign(client, action, rest) {
1442
1760
  scheduledAt: flags["scheduled-at"],
1443
1761
  }));
1444
1762
  break;
1763
+ }
1445
1764
  case "list": {
1446
1765
  const items = await campaigns.listCampaigns(client, flags.status);
1447
1766
  table(items, [
@@ -1457,14 +1776,14 @@ async function runCampaign(client, action, rest) {
1457
1776
  case "get":
1458
1777
  if (!rest[0]) {
1459
1778
  console.error("Usage: ascendkit campaign show <campaign-id>");
1460
- process.exit(1);
1779
+ return await exitCli(1);
1461
1780
  }
1462
1781
  output(await campaigns.getCampaign(client, rest[0]));
1463
1782
  break;
1464
- case "update":
1783
+ case "update": {
1465
1784
  if (!rest[0]) {
1466
1785
  console.error("Usage: ascendkit campaign update <campaign-id> [--flags]");
1467
- process.exit(1);
1786
+ return await exitCli(1);
1468
1787
  }
1469
1788
  let updateFilter;
1470
1789
  if (flags.audience) {
@@ -1473,7 +1792,7 @@ async function runCampaign(client, action, rest) {
1473
1792
  }
1474
1793
  catch {
1475
1794
  console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1476
- process.exit(1);
1795
+ return await exitCli(1);
1477
1796
  }
1478
1797
  }
1479
1798
  output(await campaigns.updateCampaign(client, rest[0], {
@@ -1483,99 +1802,82 @@ async function runCampaign(client, action, rest) {
1483
1802
  scheduledAt: flags["scheduled-at"],
1484
1803
  }));
1485
1804
  break;
1486
- case "preview":
1805
+ }
1806
+ case "preview": {
1487
1807
  if (!rest[0]) {
1488
1808
  console.error("Usage: ascendkit campaign preview <campaign-id>");
1489
- process.exit(1);
1809
+ return await exitCli(1);
1490
1810
  }
1491
- {
1492
- const detail = await campaigns.getCampaign(client, rest[0]);
1493
- if (!detail?.audienceFilter) {
1494
- console.error("Campaign has no audience filter set.");
1495
- process.exit(1);
1496
- }
1497
- 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);
1498
1815
  }
1816
+ output(await campaigns.previewAudience(client, detail.audienceFilter));
1499
1817
  break;
1818
+ }
1500
1819
  case "schedule":
1501
1820
  if (!rest[0] || !flags.at) {
1502
1821
  console.error("Usage: ascendkit campaign schedule <campaign-id> --at <datetime>");
1503
- process.exit(1);
1822
+ return await exitCli(1);
1504
1823
  }
1505
1824
  output(await campaigns.updateCampaign(client, rest[0], { scheduledAt: flags.at }));
1506
1825
  break;
1507
1826
  case "cancel":
1508
1827
  if (!rest[0]) {
1509
1828
  console.error("Usage: ascendkit campaign cancel <campaign-id>");
1510
- process.exit(1);
1829
+ return await exitCli(1);
1511
1830
  }
1512
1831
  output(await campaigns.deleteCampaign(client, rest[0]));
1513
1832
  break;
1514
1833
  case "analytics":
1515
1834
  if (!rest[0]) {
1516
1835
  console.error("Usage: ascendkit campaign analytics <campaign-id>");
1517
- process.exit(1);
1836
+ return await exitCli(1);
1518
1837
  }
1519
1838
  output(await campaigns.getCampaignAnalytics(client, rest[0]));
1520
1839
  break;
1521
1840
  default:
1522
1841
  console.error(`Unknown campaign command: ${action}`);
1523
1842
  console.error('Run "ascendkit campaign --help" for usage');
1524
- process.exit(1);
1843
+ return await exitCli(1);
1525
1844
  }
1526
1845
  }
1527
1846
  async function runEmail(client, action, rest) {
1528
1847
  const flags = parseFlags(rest);
1848
+ if (!action) {
1849
+ console.log(HELP_SECTION["email-identity"]);
1850
+ return;
1851
+ }
1529
1852
  switch (action) {
1530
- case "identity": {
1531
- const s = await email.getSettings(client);
1532
- printIdentityStatus(s);
1533
- break;
1534
- }
1535
- case "use-default": {
1536
- const s = await email.useDefaultIdentity(client);
1537
- printIdentityStatus(s);
1538
- break;
1539
- }
1540
- case "use-custom": {
1541
- if (!rest[0]) {
1542
- console.error("Usage: ascendkit email use-custom <domain> [--from-email <email>] [--from-name <name>]");
1543
- process.exit(1);
1544
- }
1545
- const s = await email.useCustomIdentity(client, rest[0], {
1546
- fromEmail: flags["from-email"],
1547
- fromName: flags["from-name"],
1548
- });
1549
- await printEmailSetup(s);
1550
- break;
1551
- }
1552
- case "settings":
1553
- if (rest[0] === "update") {
1554
- const params = {};
1555
- if (flags["from-email"])
1556
- params.fromEmail = flags["from-email"];
1557
- if (flags["from-name"])
1558
- params.fromName = flags["from-name"];
1559
- output(await email.updateSettings(client, params));
1853
+ case "settings": {
1854
+ const settings = await email.getSettings(client);
1855
+ if (flags.json === "true") {
1856
+ output(settings);
1560
1857
  }
1561
1858
  else {
1562
- output(await email.getSettings(client));
1859
+ printEmailSettingsSummary(settings);
1563
1860
  }
1564
1861
  break;
1862
+ }
1565
1863
  case "setup-domain":
1566
1864
  if (!rest[0]) {
1567
- console.error("Usage: ascendkit email setup-domain <domain>");
1568
- process.exit(1);
1865
+ console.error("Usage: ascendkit email-identity setup-domain <domain>");
1866
+ return await exitCli(1);
1569
1867
  }
1570
1868
  await printEmailSetup(await email.setupDomain(client, rest[0]));
1571
1869
  break;
1572
- case "domain-status": {
1870
+ case "status": {
1573
1871
  const interval = Math.max(5, Number.parseInt(flags.interval || "15", 10) || 15);
1574
1872
  if (flags.watch === "true") {
1575
1873
  await watchDomainStatus(client, interval);
1576
1874
  }
1577
1875
  else {
1578
- output(await email.checkDomainStatus(client));
1876
+ const settings = await email.getSettings(client);
1877
+ const domainStatus = await email.checkDomainStatus(client);
1878
+ const dnsCheck = settings.domain ? await email.checkDnsRecords(client) : null;
1879
+ const identities = await email.listIdentities(client);
1880
+ printEmailStatusSummary(settings, domainStatus, dnsCheck, identities.identities ?? []);
1579
1881
  }
1580
1882
  break;
1581
1883
  }
@@ -1584,7 +1886,7 @@ async function runEmail(client, action, rest) {
1584
1886
  const url = provider?.portalUrl;
1585
1887
  if (!url) {
1586
1888
  console.error("Could not determine DNS provider URL for this domain.");
1587
- process.exit(1);
1889
+ return await exitCli(1);
1588
1890
  }
1589
1891
  console.log(url);
1590
1892
  if (flags.open === "true") {
@@ -1595,14 +1897,74 @@ async function runEmail(client, action, rest) {
1595
1897
  case "remove-domain":
1596
1898
  output(await email.removeDomain(client));
1597
1899
  break;
1900
+ case "list": {
1901
+ const result = await email.listIdentities(client);
1902
+ printEmailIdentities(result.identities ?? []);
1903
+ break;
1904
+ }
1905
+ case "add": {
1906
+ const identityEmail = rest[0];
1907
+ if (!identityEmail) {
1908
+ console.error("Usage: ascendkit email-identity add <email> [--display-name <name>]");
1909
+ return await exitCli(1);
1910
+ }
1911
+ output(await email.createIdentity(client, {
1912
+ email: identityEmail,
1913
+ displayName: flags["display-name"],
1914
+ }));
1915
+ break;
1916
+ }
1917
+ case "resend": {
1918
+ const identityEmail = rest[0];
1919
+ if (!identityEmail) {
1920
+ console.error("Usage: ascendkit email-identity resend <email>");
1921
+ return await exitCli(1);
1922
+ }
1923
+ output(await email.resendIdentityVerification(client, identityEmail));
1924
+ break;
1925
+ }
1926
+ case "set-default": {
1927
+ const identityEmail = rest[0];
1928
+ if (!identityEmail) {
1929
+ console.error("Usage: ascendkit email-identity set-default <email> [--display-name <name>]");
1930
+ return await exitCli(1);
1931
+ }
1932
+ output(await email.setDefaultIdentity(client, {
1933
+ email: identityEmail,
1934
+ displayName: flags["display-name"],
1935
+ }));
1936
+ break;
1937
+ }
1938
+ case "remove":
1939
+ case "delete": {
1940
+ const identityEmail = rest[0];
1941
+ if (!identityEmail) {
1942
+ console.error("Usage: ascendkit email-identity remove <email>");
1943
+ return await exitCli(1);
1944
+ }
1945
+ output(await email.removeIdentity(client, identityEmail));
1946
+ break;
1947
+ }
1948
+ case "test": {
1949
+ const identityEmail = rest[0];
1950
+ if (!identityEmail || !flags.to) {
1951
+ console.error("Usage: ascendkit email-identity test <email> --to <recipient>");
1952
+ return await exitCli(1);
1953
+ }
1954
+ output(await email.sendTestEmail(client, {
1955
+ to: flags.to,
1956
+ fromIdentityEmail: identityEmail,
1957
+ }));
1958
+ break;
1959
+ }
1598
1960
  default:
1599
- console.error(`Unknown email command: ${action}`);
1600
- process.exit(1);
1961
+ console.error(`Unknown email-identity command: ${action}`);
1962
+ return await exitCli(1);
1601
1963
  }
1602
1964
  }
1603
1965
  run().catch((err) => {
1604
1966
  console.error(err.message);
1605
- process.exit(1);
1967
+ exitCli(1, err instanceof Error ? err : new Error(String(err)));
1606
1968
  });
1607
1969
  async function printEmailSetup(settings) {
1608
1970
  output(settings);
@@ -1625,28 +1987,61 @@ async function printEmailSetup(settings) {
1625
1987
  }
1626
1988
  }
1627
1989
  }
1628
- function printIdentityStatus(settings) {
1629
- const mode = settings.domain ? "customer-owned" : "ascendkit-default";
1630
- const from = settings.fromEmail || "noreply@ascendkit.dev";
1631
- const status = settings.verificationStatus || "none";
1632
- console.log(`Identity mode: ${mode}`);
1633
- console.log(`From email: ${from}`);
1634
- console.log(`From name: ${settings.fromName || "not set"}`);
1635
- if (settings.domain) {
1636
- console.log(`Domain: ${settings.domain} (${status})`);
1637
- if (status !== "verified") {
1638
- console.log("Next step: add DNS records and run `ascendkit email domain-status --watch`");
1990
+ function printEmailSettingsSummary(settings) {
1991
+ const identities = Array.isArray(settings.identities) ? settings.identities : [];
1992
+ if (!settings.domain) {
1993
+ console.log("Sender: AscendKit default");
1994
+ console.log(`From: ${settings.fromEmail || "noreply@ascendkit.dev"}`);
1995
+ console.log(`Display name: ${settings.fromName || "not set"}`);
1996
+ console.log("Domain: not configured");
1997
+ if (identities.length > 0) {
1998
+ console.log("");
1999
+ printEmailIdentities(identities);
1639
2000
  }
2001
+ return;
1640
2002
  }
1641
- else {
1642
- console.log("Next step: run `ascendkit email use-custom <domain>` to configure customer-owned identity.");
2003
+ console.log(`Domain: ${settings.domain} (${settings.verificationStatus || "unknown"})`);
2004
+ console.log("");
2005
+ printEmailIdentities(identities);
2006
+ }
2007
+ function printEmailStatusSummary(settings, domainStatus, dnsCheck, identities) {
2008
+ if (!settings.domain) {
2009
+ console.log("Domain: not configured");
2010
+ console.log("Sender: AscendKit default");
2011
+ if (identities.length > 0) {
2012
+ console.log("");
2013
+ printEmailIdentities(identities);
2014
+ }
2015
+ return;
2016
+ }
2017
+ console.log(`Domain: ${settings.domain} (${domainStatus.status || settings.verificationStatus || "unknown"})`);
2018
+ if (dnsCheck?.summary) {
2019
+ console.log(`DNS: ${dnsCheck.summary.found}/${dnsCheck.summary.total} verified`);
1643
2020
  }
2021
+ console.log("");
2022
+ printEmailIdentities(identities);
2023
+ }
2024
+ function printEmailIdentities(identities) {
2025
+ table(identities.map((identity) => ({
2026
+ email: identity.email,
2027
+ displayName: identity.displayName || "—",
2028
+ verificationStatus: identity.verificationStatus,
2029
+ isDefault: identity.isDefault ? "yes" : "",
2030
+ })), [
2031
+ { key: "email", label: "Email", width: 36 },
2032
+ { key: "displayName", label: "Display Name", width: 24 },
2033
+ { key: "verificationStatus", label: "Status", width: 12 },
2034
+ { key: "isDefault", label: "Default", width: 7 },
2035
+ ]);
1644
2036
  }
1645
2037
  async function watchDomainStatus(client, intervalSeconds) {
1646
2038
  while (true) {
1647
- const data = await email.checkDomainStatus(client);
1648
- output(data);
1649
- if (data?.status === "verified" || data?.status === "failed" || data?.status === "none") {
2039
+ const settings = await email.getSettings(client);
2040
+ const domainStatus = await email.checkDomainStatus(client);
2041
+ const dnsCheck = settings.domain ? await email.checkDnsRecords(client) : null;
2042
+ const identities = await email.listIdentities(client);
2043
+ printEmailStatusSummary(settings, domainStatus, dnsCheck, identities.identities ?? []);
2044
+ if (domainStatus?.status === "verified" || domainStatus?.status === "failed" || domainStatus?.status === "none") {
1650
2045
  return;
1651
2046
  }
1652
2047
  await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));