@ascendkit/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -11,7 +11,10 @@ import * as platform from "./commands/platform.js";
11
11
  import * as journeys from "./commands/journeys.js";
12
12
  import * as email from "./commands/email.js";
13
13
  import * as webhooks from "./commands/webhooks.js";
14
+ import * as campaigns from "./commands/campaigns.js";
15
+ import * as importCmd from "./commands/import.js";
14
16
  import { parseDelay } from "./utils/duration.js";
17
+ import { formatJourneyAnalytics, formatJourneyWithGuidance, formatNodeList, formatSingleNode, formatSingleTransition, formatTransitionList, } from "./utils/journey-format.js";
15
18
  const require = createRequire(import.meta.url);
16
19
  const { version: CLI_VERSION } = require("../package.json");
17
20
  const HELP = `ascendkit v${CLI_VERSION} - AscendKit CLI
@@ -27,15 +30,18 @@ Getting Started:
27
30
 
28
31
  Services:
29
32
  auth Authentication, providers, OAuth, users
30
- templates Email templates and versioning
33
+ template Email templates and versioning
31
34
  survey Surveys, questions, distribution, analytics
32
35
  journey Lifecycle journeys, nodes, transitions
33
- email Email settings, domain verification, DNS
36
+ email-identity Email domains, sender identities, and DNS
37
+ keystore Environment runtime key-value settings
34
38
  webhook Webhook endpoints and testing
39
+ campaign Email campaigns, scheduling, analytics
40
+ import Import users from external auth providers
35
41
 
36
42
  Project Management:
37
- projects List and create projects
38
- env List, switch, update, and promote environments
43
+ project Projects and environment selection
44
+ environment Active environment operations
39
45
  verify Check all services in the active environment
40
46
 
41
47
  Run "ascendkit help <section>" for detailed command usage.
@@ -44,70 +50,79 @@ const HELP_SECTION = {
44
50
  auth: `Usage: ascendkit auth <command>
45
51
 
46
52
  Commands:
47
- auth settings
48
- auth settings update --providers <p1,p2,...> [--email-verification <true|false>] [--waitlist <true|false>] [--password-reset <true|false>] [--session-duration <duration>]
49
- auth providers <p1,p2,...>
50
- auth oauth <provider>
53
+ auth show
54
+ auth update [--providers <p1,p2,...>] [--email-verification <true|false>] [--waitlist <true|false>] [--password-reset <true|false>] [--session-duration <duration>]
55
+ auth provider list
56
+ auth provider set <p1,p2,...>
57
+ auth oauth open <provider>
51
58
  auth oauth set <provider> --client-id <id> [--client-secret <secret> | --client-secret-stdin] [--callback-url <url>]
52
- auth users`,
53
- templates: `Usage: ascendkit templates <command>
59
+ auth oauth remove <provider>
60
+ auth user list
61
+ auth user remove <user-id>
62
+ auth user reactivate <user-id>`,
63
+ template: `Usage: ascendkit template <command>
54
64
 
55
65
  Commands:
56
- templates create --name <name> --subject <subject> --body-html <html> --body-text <text> [--slug <slug>] [--description <description>]
57
- templates list [--query <search>] [--system true|--custom true]
58
- templates get <template-id>
59
- templates update <template-id> [--subject <subject>] [--body-html <html>] [--body-text <text>] [--change-note <note>]
60
- templates delete <template-id>
61
- templates versions <template-id>
62
- templates version <template-id> <n>`,
66
+ template create --name <name> --subject <subject> --body-html <html> --body-text <text> [--slug <slug>] [--description <description>]
67
+ template list [--query <search>] [--system true|--custom true]
68
+ template show <template-id>
69
+ template update <template-id> [--subject <subject>] [--body-html <html>] [--body-text <text>] [--change-note <note>]
70
+ template remove <template-id>
71
+ template version list <template-id>
72
+ template version show <template-id> <n>`,
63
73
  survey: `Usage: ascendkit survey <command>
64
74
 
65
75
  Commands:
66
76
  survey create --name <name> [--type <nps|csat|custom>] [--definition <json>]
67
77
  survey list
68
- survey get <survey-id>
78
+ survey show <survey-id>
69
79
  survey update <survey-id> [--name <name>] [--status <draft|active|paused>] [--definition <json>]
70
- survey delete <survey-id>
80
+ survey remove <survey-id>
71
81
  survey distribute <survey-id> --users <usr_id1,usr_id2,...>
72
- survey invitations <survey-id>
82
+ survey invitation list <survey-id>
73
83
  survey analytics <survey-id>
74
- survey export-definition <survey-id> [--out <file>]
75
- survey import-definition <survey-id> --in <file>
76
-
77
- Notes:
78
- - import-definition only updates the survey definition. It does not mutate slug/name/status.`,
84
+ survey definition export <survey-id> [--out <file>]
85
+ survey definition import <survey-id> --in <file>
86
+ survey question list <survey-id>
87
+ survey question add <survey-id> --type <type> --title <title> [--name <name>] [--required <true|false>] [--choices <c1,c2,...>] [--position <n>]
88
+ survey question update <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]
89
+ survey question remove <survey-id> <question-name>
90
+ survey question reorder <survey-id> --order <name1,name2,...>`,
79
91
  journey: `Usage: ascendkit journey <command>
80
92
 
81
93
  Commands:
82
94
  journey create --name <name> --entry-event <event> --entry-node <node> [--nodes <json>] [--transitions <json>] [--description <description>] [--entry-conditions <json>] [--re-entry-policy <skip|restart>]
83
95
  journey list [--status <draft|active|paused|archived>]
84
- journey get <journey-id>
96
+ journey show <journey-id>
85
97
  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>]
86
- journey delete <journey-id>
98
+ journey remove <journey-id>
87
99
  journey activate <journey-id>
88
100
  journey pause <journey-id>
101
+ journey resume <journey-id>
89
102
  journey archive <journey-id>
90
103
  journey analytics <journey-id>
91
- journey list-nodes <journey-id>
92
- journey add-node <journey-id> --name <node-name> [--action <json>] [--terminal <true|false>]
93
- journey edit-node <journey-id> <node-name> [--action <json>] [--terminal <true|false>]
94
- journey remove-node <journey-id> <node-name>
95
- journey list-transitions <journey-id> [--from <node-name>] [--to <node-name>]
96
- journey add-transition <journey-id> --from <node-name> --to <node-name> --trigger <json> [--priority <n>] [--name <transition-name>]
97
- journey edit-transition <journey-id> <transition-name> [--trigger <json>] [--priority <n>]
98
- journey remove-transition <journey-id> <transition-name>`,
99
- email: `Usage: ascendkit email <command>
104
+ journey node list <journey-id>
105
+ journey node add <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
106
+ journey node update <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
107
+ journey node remove <journey-id> <node-name>
108
+ journey transition list <journey-id> [--from <node-name>] [--to <node-name>]
109
+ journey transition add <journey-id> --from <node-name> --to <node-name> --trigger <json> [--priority <n>] [--name <transition-name>]
110
+ journey transition update <journey-id> <transition-name> [--trigger <json>] [--priority <n>]
111
+ journey transition remove <journey-id> <transition-name>`,
112
+ "email-identity": `Usage: ascendkit email-identity <command>
100
113
 
101
114
  Commands:
102
- email settings
103
- email settings update [--from-email <email>] [--from-name <name>]
104
- email identity
105
- email use-default
106
- email use-custom <domain> [--from-email <email>] [--from-name <name>]
107
- email setup-domain <domain>
108
- email domain-status [--watch] [--interval <seconds>]
109
- email open-dns [--domain <domain>] [--open]
110
- email remove-domain`,
115
+ email-identity settings [--json]
116
+ email-identity setup-domain <domain>
117
+ email-identity status [--watch] [--interval <seconds>]
118
+ email-identity remove-domain
119
+ email-identity list
120
+ email-identity add <email> [--display-name <name>]
121
+ email-identity resend <email>
122
+ email-identity set-default <email> [--display-name <name>]
123
+ email-identity remove <email>
124
+ email-identity test <email> --to <recipient>
125
+ email-identity open-dns [--domain <domain>] [--open]`,
111
126
  webhook: `Usage: ascendkit webhook <command>
112
127
 
113
128
  Commands:
@@ -117,25 +132,77 @@ Commands:
117
132
  webhook update <webhook-id> [--url <url>] [--events <e1,e2,...>] [--status <active|inactive>]
118
133
  webhook delete <webhook-id>
119
134
  webhook test <webhook-id> [--event <event-type>]`,
120
- env: `Usage: ascendkit env <command>
135
+ campaign: `Usage: ascendkit campaign <command>
136
+
137
+ Commands:
138
+ campaign create --name <name> --template <template-id> --audience <json> [--scheduled-at <datetime>]
139
+ campaign list [--status <draft|scheduled|sending|sent|failed|cancelled>]
140
+ campaign show <campaign-id>
141
+ campaign update <campaign-id> [--name <name>] [--template <template-id>] [--audience <json>] [--scheduled-at <datetime>]
142
+ campaign preview <campaign-id>
143
+ campaign schedule <campaign-id> --at <datetime>
144
+ campaign cancel <campaign-id>
145
+ campaign analytics <campaign-id>
146
+
147
+ Notes:
148
+ - --audience is a JSON filter object, e.g. '{"tags":{"$in":["premium"]}}'
149
+ - --scheduled-at / --at accepts ISO 8601 datetime, e.g. 2026-03-15T10:00:00Z
150
+ - cancel deletes a draft/failed campaign or cancels a scheduled/sending campaign`,
151
+ environment: `Usage: ascendkit environment <command>
152
+
153
+ Commands:
154
+ environment show
155
+ environment update [<env-id>] [--name <name>] [--description <desc>]
156
+ environment promote [<env-id>] --target <tier>`,
157
+ keystore: `Usage: ascendkit keystore <command>
158
+
159
+ Commands:
160
+ keystore list
161
+ keystore set <key> <value>
162
+ keystore remove <key>`,
163
+ import: `Usage: ascendkit import <source> <action> [options]
164
+
165
+ Sources:
166
+ clerk Import users from Clerk
167
+ migration-journey Create migration email templates and journeys
121
168
 
122
169
  Commands:
123
- env list --project <project-id>
124
- env use <tier> --project <project-id>
125
- env update <env-id> --project <project-id> [--name <name>] [--description <desc>]
126
- env promote <env-id> --target <tier>`,
127
- projects: `Usage: ascendkit projects <command>
170
+ import clerk preview --api-key <key> [options]
171
+ import clerk preview --file <path> [options]
172
+ import clerk run --api-key <key> [options]
173
+ import clerk run --file <path> [options]
174
+ import migration-journey create [--from-identity <email>]
175
+
176
+ Options:
177
+ --api-key <key> Clerk secret API key (fetches users from Clerk API)
178
+ --file <path> Path to Clerk dashboard export (JSON)
179
+ --instance-url <url> Custom Clerk API URL (default: https://api.clerk.com)
180
+ --users Import users (included by default; use to select only users)
181
+ --settings Import auth settings / OAuth providers (included by default)
182
+ --from-identity <email> Email identity for migration journey emails
183
+
184
+ Use "preview" for dry-run mode and "run" to apply changes. Pass --users or --settings alone to select
185
+ only that phase (e.g. "import clerk run --users ..." imports only users, not settings).`,
186
+ project: `Usage: ascendkit project <command>
128
187
 
129
188
  Commands:
130
- projects list
131
- projects create --name <name> [--description <description>] [--services <s1,s2,...>]`,
189
+ project list
190
+ project create --name <name> [--description <description>] [--services <s1,s2,...>]
191
+ project show <project-id>
192
+ project env list <project-id>`,
132
193
  };
133
194
  function printSectionHelp(section) {
134
195
  if (!section)
135
196
  return false;
136
197
  let key = section.toLowerCase();
137
- if (key === "content")
138
- key = "templates";
198
+ if (key === "content" || key === "templates")
199
+ key = "template";
200
+ if (key === "email")
201
+ key = "email-identity";
202
+ if (key === "projects")
203
+ key = "project";
204
+ if (key === "env" || key === "environments")
205
+ key = "environment";
139
206
  const text = HELP_SECTION[key];
140
207
  if (!text)
141
208
  return false;
@@ -143,7 +210,7 @@ function printSectionHelp(section) {
143
210
  return true;
144
211
  }
145
212
  function getClient() {
146
- let publicKey = process.env.ASCENDKIT_PUBLIC_KEY;
213
+ let publicKey = process.env.ASCENDKIT_ENV_KEY;
147
214
  let apiUrl = process.env.ASCENDKIT_API_URL;
148
215
  const auth = loadAuth();
149
216
  const env = loadEnvContext();
@@ -196,6 +263,55 @@ async function readSecretFromStdin() {
196
263
  function output(data) {
197
264
  console.log(JSON.stringify(data, null, 2));
198
265
  }
266
+ function printAuthSettingsSummary(data) {
267
+ const providers = Array.isArray(data.providers) ? data.providers : [];
268
+ const features = data.features ?? {};
269
+ const featureLines = [
270
+ ["Email verification", features.emailVerification],
271
+ ["Waitlist", features.waitlist],
272
+ ["Password reset", features.passwordReset],
273
+ ["Require username", features.requireUsername],
274
+ ];
275
+ console.log(`Providers: ${providers.length > 0 ? providers.join(", ") : "none"}`);
276
+ for (const [label, enabled] of featureLines) {
277
+ console.log(`${label}: ${enabled ? "on" : "off"}`);
278
+ }
279
+ if (data.sessionDuration) {
280
+ console.log(`Session: ${data.sessionDuration}`);
281
+ }
282
+ }
283
+ function printTemplateSummary(data) {
284
+ console.log(`Template: ${data.name} (${data.id})`);
285
+ if (data.slug)
286
+ console.log(`Slug: ${data.slug}`);
287
+ console.log(`Subject: ${data.subject ?? "-"}`);
288
+ if (data.currentVersion != null)
289
+ console.log(`Version: ${data.currentVersion}`);
290
+ if (Array.isArray(data.variables) && data.variables.length > 0) {
291
+ console.log(`Variables: ${data.variables.join(", ")}`);
292
+ }
293
+ }
294
+ function printSurveySummary(data) {
295
+ console.log(`Survey: ${data.name} (${data.id})`);
296
+ console.log(`Type: ${data.type ?? "custom"} | Status: ${data.status ?? "draft"}`);
297
+ if (data.slug)
298
+ console.log(`Slug: ${data.slug}`);
299
+ const questions = Array.isArray(data.definition?.pages)
300
+ ? data.definition.pages.flatMap((page) => page.elements ?? []).length
301
+ : undefined;
302
+ if (questions != null)
303
+ console.log(`Questions: ${questions}`);
304
+ }
305
+ function printProjectSummary(data) {
306
+ console.log(`Project: ${data.id}`);
307
+ console.log(`Name: ${data.name}`);
308
+ if (Array.isArray(data.enabledServices) && data.enabledServices.length > 0) {
309
+ console.log(`Services: ${data.enabledServices.join(", ")}`);
310
+ }
311
+ if (data.environment?.publicKey) {
312
+ console.log(`Environment: ${data.environment.publicKey}`);
313
+ }
314
+ }
199
315
  function normalizeJourneyRows(data) {
200
316
  if (Array.isArray(data)) {
201
317
  return data;
@@ -276,7 +392,7 @@ async function run() {
276
392
  case "logout":
277
393
  platform.logout();
278
394
  return;
279
- case "projects":
395
+ case "project":
280
396
  if (action === "list") {
281
397
  const projects = await platform.listProjects();
282
398
  table(projects, [
@@ -285,14 +401,25 @@ async function run() {
285
401
  { key: "enabledServices", label: "Services", width: 30 },
286
402
  ]);
287
403
  }
404
+ else if (action === "show") {
405
+ if (!args[2]) {
406
+ console.error("Usage: ascendkit project show <project-id>");
407
+ process.exit(1);
408
+ }
409
+ printProjectSummary(await platform.showProject(args[2]));
410
+ }
411
+ else if (action === "env") {
412
+ await runProjectEnvironment(args.slice(2));
413
+ }
288
414
  else if (action === "create") {
289
415
  const flags = parseFlags(args.slice(2));
290
416
  if (!flags.name) {
291
- console.error("Usage: ascendkit projects create --name <name> [--description <description>] [--services <s1,s2,...>]");
417
+ console.error("Usage: ascendkit project create --name <name> [--description <description>] [--services <s1,s2,...>]");
292
418
  process.exit(1);
293
419
  }
294
420
  try {
295
- output(await platform.createProject(flags.name, flags.description, flags.services?.split(",")));
421
+ const proj = await platform.createProject(flags.name, flags.description, flags.services?.split(","));
422
+ printProjectSummary(proj);
296
423
  }
297
424
  catch (err) {
298
425
  let message = err instanceof Error ? err.message : String(err);
@@ -312,7 +439,7 @@ async function run() {
312
439
  }
313
440
  }
314
441
  else {
315
- console.error('Usage: ascendkit projects list|create');
442
+ console.error('Usage: ascendkit project list|create|show|env');
316
443
  process.exit(1);
317
444
  }
318
445
  return;
@@ -329,8 +456,11 @@ async function run() {
329
456
  case "verify":
330
457
  await runVerify();
331
458
  return;
332
- case "env":
333
- await runEnv(action, args.slice(2));
459
+ case "environment":
460
+ await runEnvironment(action, args.slice(2));
461
+ return;
462
+ case "keystore":
463
+ await runKeystore(action, args.slice(2));
334
464
  return;
335
465
  }
336
466
  // Service commands (need environment key)
@@ -339,6 +469,7 @@ async function run() {
339
469
  case "auth":
340
470
  await runAuth(client, action, args.slice(2));
341
471
  break;
472
+ case "template":
342
473
  case "templates":
343
474
  case "content":
344
475
  await runContent(client, action, args.slice(2));
@@ -349,44 +480,224 @@ async function run() {
349
480
  case "journey":
350
481
  await runJourney(client, action, args.slice(2));
351
482
  break;
352
- case "email":
483
+ case "email-identity":
353
484
  await runEmail(client, action, args.slice(2));
354
485
  break;
355
486
  case "webhook":
356
487
  await runWebhook(client, action, args.slice(2));
357
488
  break;
489
+ case "campaign":
490
+ await runCampaign(client, action, args.slice(2));
491
+ break;
492
+ case "import":
493
+ await runImport(client, action, args.slice(2));
494
+ break;
358
495
  default:
359
496
  console.error(`Unknown command: ${domain}`);
360
497
  console.error('Run "ascendkit --help" for usage');
361
498
  process.exit(1);
362
499
  }
363
500
  }
364
- async function runEnv(action, rest) {
365
- const flags = parseFlags(rest);
501
+ async function runImport(client, source, rest) {
502
+ if (!source) {
503
+ console.log(HELP_SECTION.import);
504
+ return;
505
+ }
506
+ let action = rest[0];
507
+ let args = rest;
508
+ if (source === "create-migration-journey") {
509
+ source = "migration-journey";
510
+ action = "create";
511
+ }
512
+ if (source === "clerk" && (action === "preview" || action === "run")) {
513
+ args = rest.slice(1);
514
+ }
515
+ else if (source === "clerk") {
516
+ action = flagsFromLegacy(rest).execute ? "run" : "preview";
517
+ }
518
+ const flags = parseFlags(args);
519
+ if (source === "migration-journey" && action === "create") {
520
+ const result = await importCmd.instantiateMigrationJourney(client, flags["from-identity"]);
521
+ output(result);
522
+ console.log("\nNext steps:");
523
+ console.log(" ascendkit journey list — review created journeys");
524
+ console.log(" ascendkit template list — review migration email templates");
525
+ console.log(" ascendkit journey activate <journey-id> — activate when ready");
526
+ return;
527
+ }
528
+ if (source !== "clerk") {
529
+ console.error(`Unsupported import source: ${source}`);
530
+ console.error("Supported sources: clerk");
531
+ console.error('Run "ascendkit help import" for usage');
532
+ process.exit(1);
533
+ }
534
+ const dryRun = action !== "run";
535
+ const hasUsers = flags.users !== undefined;
536
+ const hasSettings = flags.settings !== undefined;
537
+ // If neither --users nor --settings is passed, both default to true.
538
+ // If either is passed, only the specified phases run.
539
+ const importUsers = (!hasUsers && !hasSettings) || hasUsers;
540
+ const importSettings = (!hasUsers && !hasSettings) || hasSettings;
541
+ const apiKey = flags["api-key"];
542
+ const filePath = flags.file;
543
+ if (!apiKey && !filePath) {
544
+ console.error("Usage: ascendkit import clerk preview|run --api-key <key> | --file <path>");
545
+ process.exit(1);
546
+ }
547
+ let clerkUsers = [];
548
+ if (filePath) {
549
+ console.log(`Reading Clerk export from ${filePath}...`);
550
+ const rawUsers = importCmd.parseClerkExport(filePath);
551
+ clerkUsers = rawUsers.map(importCmd.transformClerkUser);
552
+ console.log(`Parsed ${clerkUsers.length} users from file.`);
553
+ }
554
+ else {
555
+ console.log("Fetching users from Clerk API...");
556
+ const rawUsers = await importCmd.fetchClerkUsers(apiKey, flags["instance-url"]);
557
+ clerkUsers = rawUsers.map(importCmd.transformClerkUser);
558
+ console.log(`Fetched ${clerkUsers.length} users from Clerk.`);
559
+ }
560
+ if (clerkUsers.length === 0 && importUsers) {
561
+ console.log("No users to import.");
562
+ return;
563
+ }
564
+ if (dryRun) {
565
+ console.log("\n--- DRY RUN (pass --execute to apply changes) ---");
566
+ }
567
+ // Batch in chunks of 500 (backend max)
568
+ const batchSize = 500;
569
+ let totalImported = 0;
570
+ const allDuplicates = [];
571
+ const allErrors = [];
572
+ const allWarnings = [];
573
+ if (importUsers) {
574
+ for (let i = 0; i < clerkUsers.length; i += batchSize) {
575
+ const batch = clerkUsers.slice(i, i + batchSize);
576
+ const batchNum = Math.floor(i / batchSize) + 1;
577
+ const totalBatches = Math.ceil(clerkUsers.length / batchSize);
578
+ if (totalBatches > 1) {
579
+ console.log(`\nBatch ${batchNum}/${totalBatches} (${batch.length} users)...`);
580
+ }
581
+ const result = await importCmd.importUsers(client, {
582
+ source: "clerk",
583
+ users: batch,
584
+ dryRun,
585
+ });
586
+ totalImported += result.imported;
587
+ allDuplicates.push(...result.duplicates);
588
+ allErrors.push(...result.errors);
589
+ allWarnings.push(...result.warnings);
590
+ }
591
+ }
592
+ else {
593
+ console.log("Skipping user import (--settings only).");
594
+ }
595
+ if (importSettings) {
596
+ const derived = importCmd.deriveSettingsFromUsers(clerkUsers);
597
+ // --providers override lets the user pick exactly which SSO to enable
598
+ const providerOverride = flags.providers;
599
+ const ssoProviders = providerOverride
600
+ ? providerOverride.split(",").map((s) => s.trim()).filter(Boolean)
601
+ : derived.providers;
602
+ const providers = [];
603
+ if (derived.hasCredentials)
604
+ providers.push("credentials");
605
+ providers.push(...ssoProviders);
606
+ console.log("\nAuth settings from user data:");
607
+ console.log(` Credentials (email/password): ${derived.hasCredentials ? "yes" : "no"}`);
608
+ console.log(` SSO providers: ${ssoProviders.length > 0 ? ssoProviders.join(", ") : "none"}`);
609
+ if (providerOverride) {
610
+ console.log(` (overridden via --providers)`);
611
+ }
612
+ if (providers.length > 0) {
613
+ const settingsPayload = {
614
+ source: "clerk",
615
+ users: [],
616
+ authSettings: { enabledProviders: providers },
617
+ dryRun,
618
+ };
619
+ const settingsResult = await importCmd.importUsers(client, settingsPayload);
620
+ allWarnings.push(...settingsResult.warnings);
621
+ if (!dryRun) {
622
+ console.log(`\nAuth settings applied: ${providers.join(", ")}`);
623
+ console.log("Configure OAuth secrets in the portal under Auth → OAuth settings.");
624
+ }
625
+ else {
626
+ console.log(`\nWill enable: ${providers.join(", ")}`);
627
+ console.log("Use --providers linkedin to pick specific SSO providers.");
628
+ }
629
+ }
630
+ }
631
+ else {
632
+ console.log("Skipping auth settings import (--users only).");
633
+ }
634
+ console.log(`\n--- Import Summary ---`);
635
+ if (importUsers) {
636
+ console.log(`Imported: ${totalImported}`);
637
+ if (allDuplicates.length > 0) {
638
+ console.log(`Duplicates: ${allDuplicates.length}`);
639
+ }
640
+ }
641
+ if (allErrors.length > 0) {
642
+ console.log(`Errors: ${allErrors.length}`);
643
+ for (const err of allErrors.slice(0, 10)) {
644
+ console.log(` - ${err.email}: ${err.reason}`);
645
+ }
646
+ if (allErrors.length > 10) {
647
+ console.log(` ... and ${allErrors.length - 10} more`);
648
+ }
649
+ }
650
+ for (const warning of allWarnings) {
651
+ console.log(`Warning: ${warning}`);
652
+ }
653
+ if (dryRun) {
654
+ console.log("\nThis was a dry run. Pass --execute to apply changes.");
655
+ }
656
+ else if (totalImported > 0) {
657
+ console.log("\nTo set up migration emails, run:\n" +
658
+ " ascendkit import migration-journey create");
659
+ }
660
+ }
661
+ function flagsFromLegacy(args) {
662
+ return parseFlags(args);
663
+ }
664
+ async function runProjectEnvironment(rest) {
665
+ const action = rest[0];
666
+ const target = rest[1];
366
667
  switch (action) {
367
668
  case "list":
368
- if (!flags.project) {
369
- console.error("Usage: ascendkit env list --project <project-id>");
669
+ if (!target) {
670
+ console.error("Usage: ascendkit project env list <project-id>");
370
671
  process.exit(1);
371
672
  }
372
- table(await platform.listEnvironments(flags.project), [
673
+ table(await platform.listEnvironments(target), [
674
+ { key: "id", label: "ID" },
373
675
  { key: "name", label: "Name", width: 20 },
374
676
  { key: "tier", label: "Tier" },
375
677
  { key: "publicKey", label: "Public Key" },
376
678
  ]);
377
- break;
378
- case "use":
379
- if (!rest[0] || !flags.project) {
380
- console.error("Usage: ascendkit env use <tier> --project <project-id>");
381
- process.exit(1);
382
- }
383
- await platform.useEnvironment(rest[0], flags.project);
384
- break;
679
+ return;
680
+ default:
681
+ console.error("Usage: ascendkit project env list <project-id>");
682
+ process.exit(1);
683
+ }
684
+ }
685
+ async function runEnvironment(action, rest) {
686
+ const flags = parseFlags(rest);
687
+ const ctx = loadEnvContext();
688
+ if (!ctx) {
689
+ console.error("No environment set. Run: ascendkit set-env <public-key>");
690
+ process.exit(1);
691
+ }
692
+ switch (action) {
693
+ case "show":
694
+ output(await platform.getEnvironment(ctx.projectId, ctx.environmentId));
695
+ return;
385
696
  case "promote": {
386
- const envId = rest[0];
697
+ const envId = rest[0] && !rest[0].startsWith("--") ? rest[0] : ctx.environmentId;
387
698
  const target = flags.target;
388
699
  if (!envId || !target) {
389
- console.error("Usage: ascendkit env promote <env-id> --target <tier>");
700
+ console.error("Usage: ascendkit environment promote [<env-id>] --target <tier>");
390
701
  process.exit(1);
391
702
  }
392
703
  try {
@@ -410,14 +721,10 @@ async function runEnv(action, rest) {
410
721
  console.error(message);
411
722
  process.exit(1);
412
723
  }
413
- break;
724
+ return;
414
725
  }
415
726
  case "update": {
416
- const envId = rest[0];
417
- if (!envId || !flags.project) {
418
- console.error("Usage: ascendkit env update <env-id> --project <project-id> [--name <name>] [--description <desc>]");
419
- process.exit(1);
420
- }
727
+ const envId = rest[0] && !rest[0].startsWith("--") ? rest[0] : ctx.environmentId;
421
728
  const name = flags.name;
422
729
  const description = flags.description;
423
730
  if (!name && description === undefined) {
@@ -425,7 +732,7 @@ async function runEnv(action, rest) {
425
732
  process.exit(1);
426
733
  }
427
734
  try {
428
- const result = await platform.updateEnvironment(flags.project, envId, name, description);
735
+ const result = await platform.updateEnvironment(ctx.projectId, envId, name, description);
429
736
  console.log("Environment updated:");
430
737
  console.log(JSON.stringify(result, null, 2));
431
738
  }
@@ -445,49 +752,137 @@ async function runEnv(action, rest) {
445
752
  console.error(message);
446
753
  process.exit(1);
447
754
  }
448
- break;
755
+ return;
449
756
  }
450
757
  default:
451
- console.error(`Unknown env command: ${action}`);
452
- console.error("Usage: ascendkit env list|use|update|promote");
758
+ console.error(`Unknown environment command: ${action}`);
759
+ console.error("Usage: ascendkit environment show|update|promote");
453
760
  process.exit(1);
454
761
  }
455
762
  }
456
- async function runAuth(client, action, rest) {
457
- const flags = parseFlags(rest);
763
+ async function runKeystore(action, rest) {
764
+ const ctx = loadEnvContext();
765
+ if (!ctx) {
766
+ console.error("No environment set. Run: ascendkit set-env <public-key>");
767
+ process.exit(1);
768
+ }
769
+ const current = await platform.getEnvironment(ctx.projectId, ctx.environmentId);
770
+ const vars = { ...(current.variables ?? {}) };
771
+ const systemVars = Array.isArray(current.systemVariables)
772
+ ? current.systemVariables
773
+ : [];
458
774
  switch (action) {
459
- case "settings":
460
- if (rest[0] === "update") {
461
- const params = {};
462
- if (flags.providers)
463
- params.providers = flags.providers.split(",");
464
- if (flags["email-verification"] || flags.waitlist || flags["password-reset"]) {
465
- params.features = {};
466
- if (flags["email-verification"])
467
- params.features.emailVerification = flags["email-verification"] === "true";
468
- if (flags.waitlist)
469
- params.features.waitlist = flags.waitlist === "true";
470
- if (flags["password-reset"])
471
- params.features.passwordReset = flags["password-reset"] === "true";
472
- }
473
- if (flags["session-duration"])
474
- params.sessionDuration = flags["session-duration"];
475
- output(await auth.updateSettings(client, params));
775
+ case "set": {
776
+ const key = rest[0];
777
+ const value = rest[1];
778
+ if (!key || value === undefined) {
779
+ console.error("Usage: ascendkit keystore set <key> <value>");
780
+ process.exit(1);
781
+ }
782
+ vars[key] = value;
783
+ await platform.updateEnvironmentVariables(ctx.projectId, ctx.environmentId, vars);
784
+ console.log(`Saved ${key}=${value}`);
785
+ return;
786
+ }
787
+ case "remove": {
788
+ const key = rest[0];
789
+ if (!key) {
790
+ console.error("Usage: ascendkit keystore remove <key>");
791
+ process.exit(1);
792
+ }
793
+ if (!(key in vars)) {
794
+ console.error(`Variable "${key}" not found.`);
795
+ process.exit(1);
796
+ }
797
+ delete vars[key];
798
+ await platform.updateEnvironmentVariables(ctx.projectId, ctx.environmentId, vars);
799
+ console.log(`Removed ${key}`);
800
+ return;
801
+ }
802
+ case "list": {
803
+ const entries = Object.entries(vars).map(([key, value]) => ({ key, value }));
804
+ if (entries.length === 0) {
805
+ console.log("No custom variables set.");
476
806
  }
477
807
  else {
478
- output(await auth.getSettings(client));
808
+ console.log("Custom variables:");
809
+ table(entries, [
810
+ { key: "key", label: "Key", width: 32 },
811
+ { key: "value", label: "Value", width: 60 },
812
+ ]);
479
813
  }
814
+ if (systemVars.length > 0) {
815
+ console.log(entries.length > 0 ? "\nSystem variables:" : "System variables:");
816
+ table(systemVars.map((item) => ({
817
+ key: item.key,
818
+ value: item.valuePreview,
819
+ availability: item.availability,
820
+ })), [
821
+ { key: "key", label: "Key", width: 24 },
822
+ { key: "value", label: "Value", width: 48 },
823
+ { key: "availability", label: "Availability", width: 22 },
824
+ ]);
825
+ }
826
+ return;
827
+ }
828
+ default:
829
+ console.error(`Unknown keystore command: ${action}`);
830
+ console.error("Usage: ascendkit keystore list|set|remove");
831
+ process.exit(1);
832
+ }
833
+ }
834
+ async function runAuth(client, action, rest) {
835
+ const flags = parseFlags(rest);
836
+ const normalizedAction = !action ? "show" :
837
+ action === "settings" ? (rest[0] === "update" ? "update" : "show") :
838
+ action === "show" ? "show" :
839
+ action === "update" ? "update" :
840
+ action;
841
+ switch (normalizedAction) {
842
+ case "show":
843
+ printAuthSettingsSummary(await auth.getSettings(client));
844
+ break;
845
+ case "update": {
846
+ const params = {};
847
+ if (flags.providers)
848
+ params.providers = flags.providers.split(",");
849
+ if (flags["email-verification"] || flags.waitlist || flags["password-reset"]) {
850
+ params.features = {};
851
+ if (flags["email-verification"])
852
+ params.features.emailVerification = flags["email-verification"] === "true";
853
+ if (flags.waitlist)
854
+ params.features.waitlist = flags.waitlist === "true";
855
+ if (flags["password-reset"])
856
+ params.features.passwordReset = flags["password-reset"] === "true";
857
+ }
858
+ if (flags["session-duration"])
859
+ params.sessionDuration = flags["session-duration"];
860
+ printAuthSettingsSummary(await auth.updateSettings(client, params));
480
861
  break;
862
+ }
863
+ case "provider":
481
864
  case "providers":
482
- if (!rest[0]) {
483
- console.error("Usage: ascendkit auth providers <p1,p2,...>");
484
- process.exit(1);
865
+ if (normalizedAction === "providers" || rest[0] === "set") {
866
+ const providersArg = normalizedAction === "providers" ? rest[0] : rest[1];
867
+ if (!providersArg) {
868
+ console.error("Usage: ascendkit auth provider set <p1,p2,...>");
869
+ process.exit(1);
870
+ }
871
+ printAuthSettingsSummary(await auth.updateProviders(client, providersArg.split(",")));
872
+ }
873
+ else {
874
+ const settings = await auth.getSettings(client);
875
+ const providers = Array.isArray(settings.providers) ? settings.providers.map((provider) => ({ provider })) : [];
876
+ if (providers.length === 0)
877
+ console.log("No providers configured.");
878
+ else
879
+ table(providers, [{ key: "provider", label: "Provider", width: 20 }]);
485
880
  }
486
- output(await auth.updateProviders(client, rest[0].split(",")));
487
881
  break;
488
882
  case "oauth": {
489
- if (rest[0] === "set") {
490
- const provider = rest[1];
883
+ const oauthAction = rest[0] === "open" || rest[0] === "set" || rest[0] === "remove" ? rest[0] : "open";
884
+ const provider = oauthAction === "open" && rest[0] !== "open" ? rest[0] : rest[1];
885
+ if (oauthAction === "set") {
491
886
  if (!provider || !flags["client-id"]) {
492
887
  console.error("Usage: ascendkit auth oauth set <provider> --client-id <id> [--client-secret <secret> | --client-secret-stdin] [--callback-url <url>]");
493
888
  process.exit(1);
@@ -507,28 +902,60 @@ async function runAuth(client, action, rest) {
507
902
  console.error("Client secret cannot be empty.");
508
903
  process.exit(1);
509
904
  }
510
- output(await auth.updateOAuthCredentials(client, provider, flags["client-id"], clientSecret, flags["callback-url"]));
905
+ await auth.updateOAuthCredentials(client, provider, flags["client-id"], clientSecret, flags["callback-url"]);
906
+ console.log(`Saved OAuth credentials for ${provider}.`);
907
+ }
908
+ else if (oauthAction === "remove") {
909
+ if (!provider) {
910
+ console.error("Usage: ascendkit auth oauth remove <provider>");
911
+ process.exit(1);
912
+ }
913
+ await auth.deleteOAuthCredentials(client, provider);
914
+ console.log(`Removed OAuth credentials for ${provider}.`);
511
915
  }
512
916
  else {
513
- if (!rest[0]) {
514
- console.error("Usage: ascendkit auth oauth <provider>");
917
+ if (!provider) {
918
+ console.error("Usage: ascendkit auth oauth open <provider>");
515
919
  process.exit(1);
516
920
  }
517
921
  const portalUrl = process.env.ASCENDKIT_PORTAL_URL ?? "http://localhost:3000";
518
- const url = auth.getOAuthSetupUrl(portalUrl, rest[0], client.currentPublicKey ?? undefined);
519
- console.log(`Opening browser to configure ${rest[0]} OAuth credentials...`);
922
+ const url = auth.getOAuthSetupUrl(portalUrl, provider, client.currentPublicKey ?? undefined);
923
+ console.log(`Opening browser to configure ${provider} OAuth credentials...`);
520
924
  console.log(url);
521
925
  openBrowser(url);
522
926
  }
523
927
  break;
524
928
  }
929
+ case "user":
525
930
  case "users":
526
- table(await auth.listUsers(client), [
527
- { key: "id", label: "ID" },
528
- { key: "email", label: "Email", width: 35 },
529
- { key: "name", label: "Name", width: 25 },
530
- { key: "status", label: "Status" },
531
- ]);
931
+ if (normalizedAction === "users" || !rest[0] || rest[0] === "list") {
932
+ table(await auth.listUsers(client), [
933
+ { key: "id", label: "ID" },
934
+ { key: "email", label: "Email", width: 35 },
935
+ { key: "name", label: "Name", width: 25 },
936
+ { key: "status", label: "Status" },
937
+ ]);
938
+ }
939
+ else if (rest[0] === "remove") {
940
+ if (!rest[1]) {
941
+ console.error("Usage: ascendkit auth user remove <user-id>");
942
+ process.exit(1);
943
+ }
944
+ await auth.deleteUser(client, rest[1]);
945
+ console.log(`Removed user ${rest[1]}.`);
946
+ }
947
+ else if (rest[0] === "reactivate") {
948
+ if (!rest[1]) {
949
+ console.error("Usage: ascendkit auth user reactivate <user-id>");
950
+ process.exit(1);
951
+ }
952
+ await auth.reactivateUser(client, rest[1]);
953
+ console.log(`Reactivated user ${rest[1]}.`);
954
+ }
955
+ else {
956
+ console.error(`Unknown auth user command: ${rest[0]}`);
957
+ process.exit(1);
958
+ }
532
959
  break;
533
960
  default:
534
961
  console.error(`Unknown auth command: ${action}`);
@@ -537,13 +964,20 @@ async function runAuth(client, action, rest) {
537
964
  }
538
965
  async function runContent(client, action, rest) {
539
966
  const flags = parseFlags(rest);
540
- switch (action) {
967
+ if (!action) {
968
+ console.log(HELP_SECTION.template);
969
+ return;
970
+ }
971
+ const normalizedAction = action === "show" || action === "get" ? "show" :
972
+ action === "remove" || action === "delete" ? "remove" :
973
+ action;
974
+ switch (normalizedAction) {
541
975
  case "create":
542
976
  if (!flags.name || !flags.subject || !flags["body-html"] || !flags["body-text"]) {
543
- console.error("Usage: ascendkit templates create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>]");
977
+ console.error("Usage: ascendkit template create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>]");
544
978
  process.exit(1);
545
979
  }
546
- output(await content.createTemplate(client, {
980
+ printTemplateSummary(await content.createTemplate(client, {
547
981
  name: flags.name, subject: flags.subject,
548
982
  bodyHtml: flags["body-html"], bodyText: flags["body-text"],
549
983
  slug: flags.slug, description: flags.description,
@@ -566,64 +1000,79 @@ async function runContent(client, action, rest) {
566
1000
  ]);
567
1001
  break;
568
1002
  }
569
- case "get":
1003
+ case "show":
570
1004
  if (!rest[0]) {
571
- console.error("Usage: ascendkit templates get <template-id>");
1005
+ console.error("Usage: ascendkit template show <template-id>");
572
1006
  process.exit(1);
573
1007
  }
574
- output(await content.getTemplate(client, rest[0]));
1008
+ printTemplateSummary(await content.getTemplate(client, rest[0]));
575
1009
  break;
576
1010
  case "update":
577
1011
  if (!rest[0]) {
578
- console.error("Usage: ascendkit templates update <template-id> [--flags]");
1012
+ console.error("Usage: ascendkit template update <template-id> [--flags]");
579
1013
  process.exit(1);
580
1014
  }
581
- output(await content.updateTemplate(client, rest[0], {
1015
+ printTemplateSummary(await content.updateTemplate(client, rest[0], {
582
1016
  subject: flags.subject,
583
1017
  bodyHtml: flags["body-html"],
584
1018
  bodyText: flags["body-text"],
585
1019
  changeNote: flags["change-note"],
586
1020
  }));
587
1021
  break;
588
- case "delete":
1022
+ case "remove":
589
1023
  if (!rest[0]) {
590
- console.error("Usage: ascendkit templates delete <template-id>");
1024
+ console.error("Usage: ascendkit template remove <template-id>");
591
1025
  process.exit(1);
592
1026
  }
593
- output(await content.deleteTemplate(client, rest[0]));
1027
+ await content.deleteTemplate(client, rest[0]);
1028
+ console.log(`Removed template ${rest[0]}.`);
594
1029
  break;
595
1030
  case "versions":
596
- if (!rest[0]) {
597
- console.error("Usage: ascendkit templates versions <template-id>");
598
- process.exit(1);
599
- }
600
- output(await content.listVersions(client, rest[0]));
601
- break;
602
1031
  case "version":
603
- if (!rest[0] || !rest[1]) {
604
- console.error("Usage: ascendkit templates version <template-id> <n>");
605
- process.exit(1);
1032
+ if (normalizedAction === "versions" || rest[0] === "list") {
1033
+ const templateId = normalizedAction === "versions" ? rest[0] : rest[1];
1034
+ if (!templateId) {
1035
+ console.error("Usage: ascendkit template version list <template-id>");
1036
+ process.exit(1);
1037
+ }
1038
+ const versions = await content.listVersions(client, templateId);
1039
+ table(versions, [
1040
+ { key: "versionNumber", label: "Version" },
1041
+ { key: "createdAt", label: "Created", width: 24 },
1042
+ { key: "changeNote", label: "Change note", width: 40 },
1043
+ ]);
1044
+ }
1045
+ else {
1046
+ const templateId = normalizedAction === "version" && rest[0] === "show" ? rest[1] : normalizedAction === "version" ? rest[0] : rest[1];
1047
+ const versionNumber = normalizedAction === "version" && rest[0] === "show" ? rest[2] : normalizedAction === "version" ? rest[1] : rest[2];
1048
+ if (!templateId || !versionNumber) {
1049
+ console.error("Usage: ascendkit template version show <template-id> <n>");
1050
+ process.exit(1);
1051
+ }
1052
+ output(await content.getVersion(client, templateId, parseInt(versionNumber, 10)));
606
1053
  }
607
- output(await content.getVersion(client, rest[0], parseInt(rest[1], 10)));
608
1054
  break;
609
1055
  default:
610
- console.error(`Unknown templates command: ${action}`);
1056
+ console.error(`Unknown template command: ${action}`);
611
1057
  process.exit(1);
612
1058
  }
613
1059
  }
614
1060
  async function runSurvey(client, action, rest) {
615
1061
  const flags = parseFlags(rest);
616
- if (!action) {
1062
+ const normalizedAction = action === "show" || action === "get" ? "show" :
1063
+ action === "remove" || action === "delete" ? "remove" :
1064
+ action;
1065
+ if (!normalizedAction) {
617
1066
  console.log(HELP_SECTION.survey);
618
1067
  return;
619
1068
  }
620
- switch (action) {
1069
+ switch (normalizedAction) {
621
1070
  case "create":
622
1071
  if (!flags.name) {
623
1072
  console.error("Usage: ascendkit survey create --name <n> [--type nps|csat|custom]");
624
1073
  process.exit(1);
625
1074
  }
626
- output(await surveys.createSurvey(client, {
1075
+ printSurveySummary(await surveys.createSurvey(client, {
627
1076
  name: flags.name,
628
1077
  type: flags.type ?? "custom",
629
1078
  definition: flags.definition ? JSON.parse(flags.definition) : undefined,
@@ -637,30 +1086,31 @@ async function runSurvey(client, action, rest) {
637
1086
  { key: "status", label: "Status" },
638
1087
  ]);
639
1088
  break;
640
- case "get":
1089
+ case "show":
641
1090
  if (!rest[0]) {
642
- console.error("Usage: ascendkit survey get <survey-id>");
1091
+ console.error("Usage: ascendkit survey show <survey-id>");
643
1092
  process.exit(1);
644
1093
  }
645
- output(await surveys.getSurvey(client, rest[0]));
1094
+ printSurveySummary(await surveys.getSurvey(client, rest[0]));
646
1095
  break;
647
1096
  case "update":
648
1097
  if (!rest[0]) {
649
1098
  console.error("Usage: ascendkit survey update <survey-id> [--flags]");
650
1099
  process.exit(1);
651
1100
  }
652
- output(await surveys.updateSurvey(client, rest[0], {
1101
+ printSurveySummary(await surveys.updateSurvey(client, rest[0], {
653
1102
  name: flags.name,
654
1103
  status: flags.status,
655
1104
  definition: flags.definition ? JSON.parse(flags.definition) : undefined,
656
1105
  }));
657
1106
  break;
658
- case "delete":
1107
+ case "remove":
659
1108
  if (!rest[0]) {
660
- console.error("Usage: ascendkit survey delete <survey-id>");
1109
+ console.error("Usage: ascendkit survey remove <survey-id>");
661
1110
  process.exit(1);
662
1111
  }
663
- output(await surveys.deleteSurvey(client, rest[0]));
1112
+ await surveys.deleteSurvey(client, rest[0]);
1113
+ console.log(`Removed survey ${rest[0]}.`);
664
1114
  break;
665
1115
  case "distribute":
666
1116
  if (!rest[0] || !flags.users) {
@@ -670,11 +1120,19 @@ async function runSurvey(client, action, rest) {
670
1120
  output(await surveys.distributeSurvey(client, rest[0], flags.users.split(",")));
671
1121
  break;
672
1122
  case "invitations":
673
- if (!rest[0]) {
674
- console.error("Usage: ascendkit survey invitations <survey-id>");
1123
+ case "invitation":
1124
+ if (normalizedAction === "invitations" || rest[0] === "list") {
1125
+ const surveyId = normalizedAction === "invitations" ? rest[0] : rest[1];
1126
+ if (!surveyId) {
1127
+ console.error("Usage: ascendkit survey invitation list <survey-id>");
1128
+ process.exit(1);
1129
+ }
1130
+ output(await surveys.listInvitations(client, surveyId));
1131
+ }
1132
+ else {
1133
+ console.error(`Unknown survey invitation command: ${rest[0]}`);
675
1134
  process.exit(1);
676
1135
  }
677
- output(await surveys.listInvitations(client, rest[0]));
678
1136
  break;
679
1137
  case "analytics":
680
1138
  if (!rest[0]) {
@@ -683,96 +1141,121 @@ async function runSurvey(client, action, rest) {
683
1141
  }
684
1142
  output(await surveys.getAnalytics(client, rest[0]));
685
1143
  break;
686
- case "export-definition": {
687
- if (!rest[0]) {
688
- console.error("Usage: ascendkit survey export-definition <survey-id> [--out <file>]");
689
- process.exit(1);
690
- }
691
- const survey = await surveys.getSurvey(client, rest[0]);
692
- const text = `${JSON.stringify(survey.definition ?? {}, null, 2)}\n`;
693
- const outPath = flags.out || flags.file;
694
- if (outPath) {
695
- writeFileSync(outPath, text, "utf8");
696
- console.log(`Wrote survey definition to ${outPath}`);
1144
+ case "export-definition":
1145
+ case "import-definition":
1146
+ case "definition": {
1147
+ const definitionAction = normalizedAction === "definition" ? rest[0] :
1148
+ normalizedAction === "export-definition" ? "export" :
1149
+ "import";
1150
+ const surveyId = normalizedAction === "definition" ? rest[1] : rest[0];
1151
+ if (definitionAction === "export") {
1152
+ if (!surveyId) {
1153
+ console.error("Usage: ascendkit survey definition export <survey-id> [--out <file>]");
1154
+ process.exit(1);
1155
+ }
1156
+ const survey = await surveys.getSurvey(client, surveyId);
1157
+ const text = `${JSON.stringify(survey.definition ?? {}, null, 2)}\n`;
1158
+ const outPath = flags.out || flags.file;
1159
+ if (outPath) {
1160
+ writeFileSync(outPath, text, "utf8");
1161
+ console.log(`Wrote survey definition to ${outPath}`);
1162
+ }
1163
+ else {
1164
+ process.stdout.write(text);
1165
+ }
697
1166
  }
698
1167
  else {
699
- process.stdout.write(text);
700
- }
701
- break;
702
- }
703
- case "import-definition": {
704
- if (!rest[0] || !(flags.in || flags.file)) {
705
- console.error("Usage: ascendkit survey import-definition <survey-id> --in <file>");
706
- process.exit(1);
707
- }
708
- const inPath = flags.in || flags.file;
709
- const raw = readFileSync(inPath, "utf8");
710
- const parsed = JSON.parse(raw);
711
- const candidate = parsed && typeof parsed === "object" && "definition" in parsed
712
- ? parsed.definition
713
- : parsed;
714
- if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
715
- console.error("Invalid definition JSON: expected an object or { definition: object }");
716
- process.exit(1);
1168
+ if (!surveyId || !(flags.in || flags.file)) {
1169
+ console.error("Usage: ascendkit survey definition import <survey-id> --in <file>");
1170
+ process.exit(1);
1171
+ }
1172
+ const inPath = flags.in || flags.file;
1173
+ const raw = readFileSync(inPath, "utf8");
1174
+ const parsed = JSON.parse(raw);
1175
+ const candidate = parsed && typeof parsed === "object" && "definition" in parsed
1176
+ ? parsed.definition
1177
+ : parsed;
1178
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
1179
+ console.error("Invalid definition JSON: expected an object or { definition: object }");
1180
+ process.exit(1);
1181
+ }
1182
+ printSurveySummary(await surveys.updateSurvey(client, surveyId, {
1183
+ definition: candidate,
1184
+ }));
717
1185
  }
718
- output(await surveys.updateSurvey(client, rest[0], {
719
- definition: candidate,
720
- }));
721
1186
  break;
722
1187
  }
723
1188
  case "list-questions":
724
- if (!rest[0]) {
725
- console.error("Usage: ascendkit survey list-questions <survey-id>");
1189
+ case "add-question":
1190
+ case "edit-question":
1191
+ case "remove-question":
1192
+ case "reorder-questions":
1193
+ case "question": {
1194
+ const questionAction = normalizedAction === "question" ? rest[0] :
1195
+ normalizedAction === "list-questions" ? "list" :
1196
+ normalizedAction === "add-question" ? "add" :
1197
+ normalizedAction === "edit-question" ? "update" :
1198
+ normalizedAction === "remove-question" ? "remove" :
1199
+ "reorder";
1200
+ const surveyId = normalizedAction === "question" ? rest[1] : rest[0];
1201
+ const questionName = normalizedAction === "question" ? rest[2] : rest[1];
1202
+ if (!surveyId) {
1203
+ console.error("Usage: ascendkit survey question list <survey-id>");
726
1204
  process.exit(1);
727
1205
  }
728
- output(await surveys.listQuestions(client, rest[0]));
729
- break;
730
- case "add-question": {
731
- if (!rest[0] || !flags.type || !flags.title) {
732
- console.error("Usage: ascendkit survey add-question <survey-id> --type <type> --title <title> [--name <name>] [--required <true|false>] [--choices <c1,c2,...>] [--position <n>]");
733
- process.exit(1);
1206
+ if (questionAction === "list") {
1207
+ output(await surveys.listQuestions(client, surveyId));
734
1208
  }
735
- const params = { type: flags.type, title: flags.title };
736
- if (flags.name)
737
- params.name = flags.name;
738
- if (flags.required)
739
- params.isRequired = flags.required === "true";
740
- if (flags.choices)
741
- params.choices = flags.choices.split(",");
742
- if (flags.position != null)
743
- params.position = Number(flags.position);
744
- output(await surveys.addQuestion(client, rest[0], params));
745
- break;
746
- }
747
- case "edit-question": {
748
- if (!rest[0] || !rest[1]) {
749
- console.error("Usage: ascendkit survey edit-question <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]");
750
- process.exit(1);
1209
+ else if (questionAction === "add") {
1210
+ if (!flags.type || !flags.title) {
1211
+ 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);
1213
+ }
1214
+ const params = { type: flags.type, title: flags.title };
1215
+ if (flags.name)
1216
+ params.name = flags.name;
1217
+ if (flags.required)
1218
+ params.isRequired = flags.required === "true";
1219
+ if (flags.choices)
1220
+ params.choices = flags.choices.split(",");
1221
+ if (flags.position != null)
1222
+ params.position = Number(flags.position);
1223
+ output(await surveys.addQuestion(client, surveyId, params));
1224
+ }
1225
+ else if (questionAction === "update") {
1226
+ if (!questionName) {
1227
+ console.error("Usage: ascendkit survey question update <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]");
1228
+ process.exit(1);
1229
+ }
1230
+ const params = {};
1231
+ if (flags.title)
1232
+ params.title = flags.title;
1233
+ if (flags.required)
1234
+ params.isRequired = flags.required === "true";
1235
+ if (flags.choices)
1236
+ params.choices = flags.choices.split(",");
1237
+ output(await surveys.editQuestion(client, surveyId, questionName, params));
1238
+ }
1239
+ else if (questionAction === "remove") {
1240
+ if (!questionName) {
1241
+ console.error("Usage: ascendkit survey question remove <survey-id> <question-name>");
1242
+ process.exit(1);
1243
+ }
1244
+ output(await surveys.removeQuestion(client, surveyId, questionName));
751
1245
  }
752
- const params = {};
753
- if (flags.title)
754
- params.title = flags.title;
755
- if (flags.required)
756
- params.isRequired = flags.required === "true";
757
- if (flags.choices)
758
- params.choices = flags.choices.split(",");
759
- output(await surveys.editQuestion(client, rest[0], rest[1], params));
760
- break;
761
- }
762
- case "remove-question":
763
- if (!rest[0] || !rest[1]) {
764
- console.error("Usage: ascendkit survey remove-question <survey-id> <question-name>");
765
- process.exit(1);
1246
+ else if (questionAction === "reorder") {
1247
+ if (!flags.order) {
1248
+ console.error("Usage: ascendkit survey question reorder <survey-id> --order <name1,name2,...>");
1249
+ process.exit(1);
1250
+ }
1251
+ output(await surveys.reorderQuestions(client, surveyId, flags.order.split(",")));
766
1252
  }
767
- output(await surveys.removeQuestion(client, rest[0], rest[1]));
768
- break;
769
- case "reorder-questions":
770
- if (!rest[0] || !flags.order) {
771
- console.error("Usage: ascendkit survey reorder-questions <survey-id> --order <name1,name2,...>");
1253
+ else {
1254
+ console.error(`Unknown survey question command: ${questionAction}`);
772
1255
  process.exit(1);
773
1256
  }
774
- output(await surveys.reorderQuestions(client, rest[0], flags.order.split(",")));
775
1257
  break;
1258
+ }
776
1259
  default:
777
1260
  console.error(`Unknown survey command: ${action}`);
778
1261
  console.error('Run "ascendkit survey --help" for usage');
@@ -797,7 +1280,7 @@ function runStatus() {
797
1280
  }
798
1281
  else {
799
1282
  console.log(" No environment set. Run: ascendkit set-env <public-key>");
800
- console.log(" List environments: ascendkit env list --project <project-id>");
1283
+ console.log(" List environments: ascendkit project env list <project-id>");
801
1284
  }
802
1285
  console.log();
803
1286
  }
@@ -861,6 +1344,34 @@ async function runVerify() {
861
1344
  console.log();
862
1345
  }
863
1346
  async function runJourney(client, action, rest) {
1347
+ if (!action) {
1348
+ console.log(HELP_SECTION.journey);
1349
+ return;
1350
+ }
1351
+ if (action === "show")
1352
+ action = "get";
1353
+ if (action === "remove")
1354
+ action = "delete";
1355
+ if (action === "node") {
1356
+ const nodeAction = rest[0];
1357
+ action =
1358
+ nodeAction === "list" ? "list-nodes" :
1359
+ nodeAction === "add" ? "add-node" :
1360
+ nodeAction === "update" ? "edit-node" :
1361
+ nodeAction === "remove" ? "remove-node" :
1362
+ action;
1363
+ rest = rest.slice(1);
1364
+ }
1365
+ if (action === "transition") {
1366
+ const transitionAction = rest[0];
1367
+ action =
1368
+ transitionAction === "list" ? "list-transitions" :
1369
+ transitionAction === "add" ? "add-transition" :
1370
+ transitionAction === "update" ? "edit-transition" :
1371
+ transitionAction === "remove" ? "remove-transition" :
1372
+ action;
1373
+ rest = rest.slice(1);
1374
+ }
864
1375
  const flags = parseFlags(rest);
865
1376
  switch (action) {
866
1377
  case "create":
@@ -868,7 +1379,7 @@ async function runJourney(client, action, rest) {
868
1379
  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>]");
869
1380
  process.exit(1);
870
1381
  }
871
- output(await journeys.createJourney(client, {
1382
+ console.log(formatJourneyWithGuidance((await journeys.createJourney(client, {
872
1383
  name: flags.name,
873
1384
  entryEvent: flags["entry-event"],
874
1385
  entryNode: flags["entry-node"],
@@ -877,7 +1388,7 @@ async function runJourney(client, action, rest) {
877
1388
  description: flags.description,
878
1389
  entryConditions: flags["entry-conditions"] ? JSON.parse(flags["entry-conditions"]) : undefined,
879
1390
  reEntryPolicy: flags["re-entry-policy"],
880
- }));
1391
+ }))));
881
1392
  break;
882
1393
  case "list": {
883
1394
  const opts = {};
@@ -895,17 +1406,17 @@ async function runJourney(client, action, rest) {
895
1406
  }
896
1407
  case "get":
897
1408
  if (!rest[0]) {
898
- console.error("Usage: ascendkit journey get <journey-id>");
1409
+ console.error("Usage: ascendkit journey show <journey-id>");
899
1410
  process.exit(1);
900
1411
  }
901
- output(await journeys.getJourney(client, rest[0]));
1412
+ console.log(formatJourneyWithGuidance(await journeys.getJourney(client, rest[0])));
902
1413
  break;
903
1414
  case "update":
904
1415
  if (!rest[0]) {
905
1416
  console.error("Usage: ascendkit journey update <journey-id> [--flags]");
906
1417
  process.exit(1);
907
1418
  }
908
- output(await journeys.updateJourney(client, rest[0], {
1419
+ console.log(formatJourneyWithGuidance((await journeys.updateJourney(client, rest[0], {
909
1420
  name: flags.name,
910
1421
  description: flags.description,
911
1422
  entryEvent: flags["entry-event"],
@@ -914,97 +1425,128 @@ async function runJourney(client, action, rest) {
914
1425
  reEntryPolicy: flags["re-entry-policy"],
915
1426
  nodes: flags.nodes ? JSON.parse(flags.nodes) : undefined,
916
1427
  transitions: flags.transitions ? JSON.parse(flags.transitions) : undefined,
917
- }));
1428
+ }))));
918
1429
  break;
919
1430
  case "delete":
920
1431
  if (!rest[0]) {
921
- console.error("Usage: ascendkit journey delete <journey-id>");
1432
+ console.error("Usage: ascendkit journey remove <journey-id>");
922
1433
  process.exit(1);
923
1434
  }
924
- output(await journeys.deleteJourney(client, rest[0]));
1435
+ await journeys.deleteJourney(client, rest[0]);
1436
+ console.log(`Deleted journey ${rest[0]}.`);
925
1437
  break;
926
1438
  case "activate":
927
1439
  if (!rest[0]) {
928
1440
  console.error("Usage: ascendkit journey activate <journey-id>");
929
1441
  process.exit(1);
930
1442
  }
931
- output(await journeys.activateJourney(client, rest[0]));
1443
+ console.log(formatJourneyWithGuidance(await journeys.activateJourney(client, rest[0])));
932
1444
  break;
933
1445
  case "pause":
934
1446
  if (!rest[0]) {
935
1447
  console.error("Usage: ascendkit journey pause <journey-id>");
936
1448
  process.exit(1);
937
1449
  }
938
- output(await journeys.pauseJourney(client, rest[0]));
1450
+ console.log(formatJourneyWithGuidance(await journeys.pauseJourney(client, rest[0])));
1451
+ break;
1452
+ case "resume":
1453
+ if (!rest[0]) {
1454
+ console.error("Usage: ascendkit journey resume <journey-id>");
1455
+ process.exit(1);
1456
+ }
1457
+ console.log(formatJourneyWithGuidance(await journeys.resumeJourney(client, rest[0])));
939
1458
  break;
940
1459
  case "archive":
941
1460
  if (!rest[0]) {
942
1461
  console.error("Usage: ascendkit journey archive <journey-id>");
943
1462
  process.exit(1);
944
1463
  }
945
- output(await journeys.archiveJourney(client, rest[0]));
1464
+ console.log(formatJourneyWithGuidance(await journeys.archiveJourney(client, rest[0])));
946
1465
  break;
947
1466
  case "analytics":
948
1467
  if (!rest[0]) {
949
1468
  console.error("Usage: ascendkit journey analytics <journey-id>");
950
1469
  process.exit(1);
951
1470
  }
952
- output(await journeys.getJourneyAnalytics(client, rest[0]));
1471
+ console.log(formatJourneyAnalytics(await journeys.getJourneyAnalytics(client, rest[0])));
953
1472
  break;
954
1473
  case "list-nodes":
955
1474
  if (!rest[0]) {
956
- console.error("Usage: ascendkit journey list-nodes <journey-id>");
1475
+ console.error("Usage: ascendkit journey node list <journey-id>");
957
1476
  process.exit(1);
958
1477
  }
959
- output(await journeys.listNodes(client, rest[0]));
1478
+ console.log(formatNodeList(await journeys.listNodes(client, rest[0])));
960
1479
  break;
961
1480
  case "add-node": {
962
1481
  if (!rest[0] || !flags.name) {
963
- console.error("Usage: ascendkit journey add-node <journey-id> --name <node-name> [--action <json>] [--terminal <true|false>]");
1482
+ console.error("Usage: ascendkit journey node add <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
964
1483
  process.exit(1);
965
1484
  }
966
1485
  const params = { name: flags.name };
967
1486
  if (flags.action)
968
1487
  params.action = JSON.parse(flags.action);
1488
+ if (flags["email-id"]) {
1489
+ if (!params.action || params.action.type !== "send_email") {
1490
+ console.error("--email-id requires a send_email action (use --action '{\"type\": \"send_email\", \"templateSlug\": \"...\"}')");
1491
+ process.exit(1);
1492
+ }
1493
+ params.action.fromIdentityEmail = flags["email-id"];
1494
+ }
969
1495
  if (flags.terminal)
970
1496
  params.terminal = flags.terminal === "true";
971
- output(await journeys.addNode(client, rest[0], params));
1497
+ console.log(formatSingleNode(await journeys.addNode(client, rest[0], params), "Added", flags.name));
972
1498
  break;
973
1499
  }
974
1500
  case "edit-node": {
975
1501
  if (!rest[0] || !rest[1]) {
976
- console.error("Usage: ascendkit journey edit-node <journey-id> <node-name> [--action <json>] [--terminal <true|false>]");
1502
+ console.error("Usage: ascendkit journey node update <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
977
1503
  process.exit(1);
978
1504
  }
979
1505
  const params = {};
980
1506
  if (flags.action)
981
1507
  params.action = JSON.parse(flags.action);
1508
+ if (flags["email-id"]) {
1509
+ if (!params.action) {
1510
+ const nodesData = await journeys.listNodes(client, rest[0]);
1511
+ const current = nodesData.nodes?.find((n) => n.name === rest[1]);
1512
+ if (!current?.action || current.action.type !== "send_email") {
1513
+ console.error("--email-id can only be set on send_email nodes");
1514
+ process.exit(1);
1515
+ }
1516
+ params.action = current.action;
1517
+ }
1518
+ else if (params.action.type !== "send_email") {
1519
+ console.error("--email-id requires a send_email action");
1520
+ process.exit(1);
1521
+ }
1522
+ params.action.fromIdentityEmail = flags["email-id"];
1523
+ }
982
1524
  if (flags.terminal)
983
1525
  params.terminal = flags.terminal === "true";
984
- output(await journeys.editNode(client, rest[0], rest[1], params));
1526
+ console.log(formatSingleNode(await journeys.editNode(client, rest[0], rest[1], params), "Updated", rest[1]));
985
1527
  break;
986
1528
  }
987
1529
  case "remove-node":
988
1530
  if (!rest[0] || !rest[1]) {
989
- console.error("Usage: ascendkit journey remove-node <journey-id> <node-name>");
1531
+ console.error("Usage: ascendkit journey node remove <journey-id> <node-name>");
990
1532
  process.exit(1);
991
1533
  }
992
- output(await journeys.removeNode(client, rest[0], rest[1]));
1534
+ console.log(formatSingleNode(await journeys.removeNode(client, rest[0], rest[1]), "Removed", rest[1]));
993
1535
  break;
994
1536
  case "list-transitions": {
995
1537
  if (!rest[0]) {
996
- console.error("Usage: ascendkit journey list-transitions <journey-id> [--from <node-name>] [--to <node-name>]");
1538
+ console.error("Usage: ascendkit journey transition list <journey-id> [--from <node-name>] [--to <node-name>]");
997
1539
  process.exit(1);
998
1540
  }
999
- output(await journeys.listTransitions(client, rest[0], {
1541
+ console.log(formatTransitionList(await journeys.listTransitions(client, rest[0], {
1000
1542
  from_node: flags.from,
1001
1543
  to_node: flags.to,
1002
- }));
1544
+ })));
1003
1545
  break;
1004
1546
  }
1005
1547
  case "add-transition": {
1006
1548
  if (!rest[0] || !flags.from || !flags.to || !(flags.on || flags.after || flags.trigger)) {
1007
- 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>]");
1549
+ 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>]");
1008
1550
  process.exit(1);
1009
1551
  }
1010
1552
  let trigger;
@@ -1026,12 +1568,13 @@ async function runJourney(client, action, rest) {
1026
1568
  params.priority = Number(flags.priority);
1027
1569
  if (flags.name)
1028
1570
  params.name = flags.name;
1029
- output(await journeys.addTransition(client, rest[0], params));
1571
+ const transitionName = flags.name || `${flags.from}-to-${flags.to}`;
1572
+ console.log(formatSingleTransition(await journeys.addTransition(client, rest[0], params), "Added", transitionName));
1030
1573
  break;
1031
1574
  }
1032
1575
  case "edit-transition": {
1033
1576
  if (!rest[0] || !rest[1]) {
1034
- console.error("Usage: ascendkit journey edit-transition <journey-id> <transition-name> [--on <event>] [--after <delay>] [--trigger <json>] [--priority <n>]");
1577
+ console.error("Usage: ascendkit journey transition update <journey-id> <transition-name> [--on <event>] [--after <delay>] [--trigger <json>] [--priority <n>]");
1035
1578
  process.exit(1);
1036
1579
  }
1037
1580
  const params = {};
@@ -1046,15 +1589,15 @@ async function runJourney(client, action, rest) {
1046
1589
  }
1047
1590
  if (flags.priority != null)
1048
1591
  params.priority = Number(flags.priority);
1049
- output(await journeys.editTransition(client, rest[0], rest[1], params));
1592
+ console.log(formatSingleTransition(await journeys.editTransition(client, rest[0], rest[1], params), "Updated", rest[1]));
1050
1593
  break;
1051
1594
  }
1052
1595
  case "remove-transition":
1053
1596
  if (!rest[0] || !rest[1]) {
1054
- console.error("Usage: ascendkit journey remove-transition <journey-id> <transition-name>");
1597
+ console.error("Usage: ascendkit journey transition remove <journey-id> <transition-name>");
1055
1598
  process.exit(1);
1056
1599
  }
1057
- output(await journeys.removeTransition(client, rest[0], rest[1]));
1600
+ console.log(formatSingleTransition(await journeys.removeTransition(client, rest[0], rest[1]), "Removed", rest[1]));
1058
1601
  break;
1059
1602
  default:
1060
1603
  console.error(`Unknown journey command: ${action}`);
@@ -1089,17 +1632,22 @@ async function runWebhook(client, action, rest) {
1089
1632
  }
1090
1633
  output(await webhooks.getWebhook(client, rest[0]));
1091
1634
  break;
1092
- case "update":
1635
+ case "update": {
1093
1636
  if (!rest[0]) {
1094
1637
  console.error("Usage: ascendkit webhook update <webhook-id> [--flags]");
1095
1638
  process.exit(1);
1096
1639
  }
1097
- output(await webhooks.updateWebhook(client, rest[0], {
1640
+ const updated = await webhooks.updateWebhook(client, rest[0], {
1098
1641
  url: flags.url,
1099
1642
  events: flags.events ? flags.events.split(",") : undefined,
1100
1643
  status: flags.status,
1101
- }));
1644
+ });
1645
+ console.log(`Updated: ${updated.id}`);
1646
+ console.log(`URL: ${updated.url}`);
1647
+ console.log(`Status: ${updated.status}`);
1648
+ console.log(`Events: ${Array.isArray(updated.events) ? updated.events.join(", ") : updated.events}`);
1102
1649
  break;
1650
+ }
1103
1651
  case "delete":
1104
1652
  if (!rest[0]) {
1105
1653
  console.error("Usage: ascendkit webhook delete <webhook-id>");
@@ -1120,58 +1668,150 @@ async function runWebhook(client, action, rest) {
1120
1668
  process.exit(1);
1121
1669
  }
1122
1670
  }
1123
- async function runEmail(client, action, rest) {
1671
+ async function runCampaign(client, action, rest) {
1124
1672
  const flags = parseFlags(rest);
1673
+ if (!action) {
1674
+ console.log(HELP_SECTION.campaign);
1675
+ return;
1676
+ }
1125
1677
  switch (action) {
1126
- case "identity": {
1127
- const s = await email.getSettings(client);
1128
- printIdentityStatus(s);
1678
+ case "create":
1679
+ if (!flags.name || !flags.template || !flags.audience) {
1680
+ console.error("Usage: ascendkit campaign create --name <name> --template <template-id> --audience <json> [--scheduled-at <datetime>]");
1681
+ process.exit(1);
1682
+ }
1683
+ let createFilter;
1684
+ try {
1685
+ createFilter = JSON.parse(flags.audience);
1686
+ }
1687
+ catch {
1688
+ console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1689
+ process.exit(1);
1690
+ }
1691
+ output(await campaigns.createCampaign(client, {
1692
+ name: flags.name,
1693
+ templateId: flags.template,
1694
+ audienceFilter: createFilter,
1695
+ scheduledAt: flags["scheduled-at"],
1696
+ }));
1129
1697
  break;
1130
- }
1131
- case "use-default": {
1132
- const s = await email.useDefaultIdentity(client);
1133
- printIdentityStatus(s);
1698
+ case "list": {
1699
+ const items = await campaigns.listCampaigns(client, flags.status);
1700
+ table(items, [
1701
+ { key: "id", label: "ID" },
1702
+ { key: "name", label: "Name", width: 30 },
1703
+ { key: "status", label: "Status" },
1704
+ { key: "scheduledAt", label: "Scheduled", width: 22 },
1705
+ { key: "templateId", label: "Template" },
1706
+ ]);
1134
1707
  break;
1135
1708
  }
1136
- case "use-custom": {
1709
+ case "show":
1710
+ case "get":
1137
1711
  if (!rest[0]) {
1138
- console.error("Usage: ascendkit email use-custom <domain> [--from-email <email>] [--from-name <name>]");
1712
+ console.error("Usage: ascendkit campaign show <campaign-id>");
1139
1713
  process.exit(1);
1140
1714
  }
1141
- const s = await email.useCustomIdentity(client, rest[0], {
1142
- fromEmail: flags["from-email"],
1143
- fromName: flags["from-name"],
1144
- });
1145
- await printEmailSetup(s);
1715
+ output(await campaigns.getCampaign(client, rest[0]));
1146
1716
  break;
1147
- }
1148
- case "settings":
1149
- if (rest[0] === "update") {
1150
- const params = {};
1151
- if (flags["from-email"])
1152
- params.fromEmail = flags["from-email"];
1153
- if (flags["from-name"])
1154
- params.fromName = flags["from-name"];
1155
- output(await email.updateSettings(client, params));
1717
+ case "update":
1718
+ if (!rest[0]) {
1719
+ console.error("Usage: ascendkit campaign update <campaign-id> [--flags]");
1720
+ process.exit(1);
1721
+ }
1722
+ let updateFilter;
1723
+ if (flags.audience) {
1724
+ try {
1725
+ updateFilter = JSON.parse(flags.audience);
1726
+ }
1727
+ catch {
1728
+ console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1729
+ process.exit(1);
1730
+ }
1731
+ }
1732
+ output(await campaigns.updateCampaign(client, rest[0], {
1733
+ name: flags.name,
1734
+ templateId: flags.template,
1735
+ audienceFilter: updateFilter,
1736
+ scheduledAt: flags["scheduled-at"],
1737
+ }));
1738
+ break;
1739
+ case "preview":
1740
+ if (!rest[0]) {
1741
+ console.error("Usage: ascendkit campaign preview <campaign-id>");
1742
+ process.exit(1);
1743
+ }
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));
1751
+ }
1752
+ break;
1753
+ case "schedule":
1754
+ if (!rest[0] || !flags.at) {
1755
+ console.error("Usage: ascendkit campaign schedule <campaign-id> --at <datetime>");
1756
+ process.exit(1);
1757
+ }
1758
+ output(await campaigns.updateCampaign(client, rest[0], { scheduledAt: flags.at }));
1759
+ break;
1760
+ case "cancel":
1761
+ if (!rest[0]) {
1762
+ console.error("Usage: ascendkit campaign cancel <campaign-id>");
1763
+ process.exit(1);
1764
+ }
1765
+ output(await campaigns.deleteCampaign(client, rest[0]));
1766
+ break;
1767
+ case "analytics":
1768
+ if (!rest[0]) {
1769
+ console.error("Usage: ascendkit campaign analytics <campaign-id>");
1770
+ process.exit(1);
1771
+ }
1772
+ output(await campaigns.getCampaignAnalytics(client, rest[0]));
1773
+ break;
1774
+ default:
1775
+ console.error(`Unknown campaign command: ${action}`);
1776
+ console.error('Run "ascendkit campaign --help" for usage');
1777
+ process.exit(1);
1778
+ }
1779
+ }
1780
+ async function runEmail(client, action, rest) {
1781
+ const flags = parseFlags(rest);
1782
+ if (!action) {
1783
+ console.log(HELP_SECTION["email-identity"]);
1784
+ return;
1785
+ }
1786
+ switch (action) {
1787
+ case "settings": {
1788
+ const settings = await email.getSettings(client);
1789
+ if (flags.json === "true") {
1790
+ output(settings);
1156
1791
  }
1157
1792
  else {
1158
- output(await email.getSettings(client));
1793
+ printEmailSettingsSummary(settings);
1159
1794
  }
1160
1795
  break;
1796
+ }
1161
1797
  case "setup-domain":
1162
1798
  if (!rest[0]) {
1163
- console.error("Usage: ascendkit email setup-domain <domain>");
1799
+ console.error("Usage: ascendkit email-identity setup-domain <domain>");
1164
1800
  process.exit(1);
1165
1801
  }
1166
1802
  await printEmailSetup(await email.setupDomain(client, rest[0]));
1167
1803
  break;
1168
- case "domain-status": {
1804
+ case "status": {
1169
1805
  const interval = Math.max(5, Number.parseInt(flags.interval || "15", 10) || 15);
1170
1806
  if (flags.watch === "true") {
1171
1807
  await watchDomainStatus(client, interval);
1172
1808
  }
1173
1809
  else {
1174
- output(await email.checkDomainStatus(client));
1810
+ const settings = await email.getSettings(client);
1811
+ const domainStatus = await email.checkDomainStatus(client);
1812
+ const dnsCheck = settings.domain ? await email.checkDnsRecords(client) : null;
1813
+ const identities = await email.listIdentities(client);
1814
+ printEmailStatusSummary(settings, domainStatus, dnsCheck, identities.identities ?? []);
1175
1815
  }
1176
1816
  break;
1177
1817
  }
@@ -1191,8 +1831,68 @@ async function runEmail(client, action, rest) {
1191
1831
  case "remove-domain":
1192
1832
  output(await email.removeDomain(client));
1193
1833
  break;
1834
+ case "list": {
1835
+ const result = await email.listIdentities(client);
1836
+ printEmailIdentities(result.identities ?? []);
1837
+ break;
1838
+ }
1839
+ case "add": {
1840
+ const identityEmail = rest[0];
1841
+ if (!identityEmail) {
1842
+ console.error("Usage: ascendkit email-identity add <email> [--display-name <name>]");
1843
+ process.exit(1);
1844
+ }
1845
+ output(await email.createIdentity(client, {
1846
+ email: identityEmail,
1847
+ displayName: flags["display-name"],
1848
+ }));
1849
+ break;
1850
+ }
1851
+ case "resend": {
1852
+ const identityEmail = rest[0];
1853
+ if (!identityEmail) {
1854
+ console.error("Usage: ascendkit email-identity resend <email>");
1855
+ process.exit(1);
1856
+ }
1857
+ output(await email.resendIdentityVerification(client, identityEmail));
1858
+ break;
1859
+ }
1860
+ case "set-default": {
1861
+ const identityEmail = rest[0];
1862
+ if (!identityEmail) {
1863
+ console.error("Usage: ascendkit email-identity set-default <email> [--display-name <name>]");
1864
+ process.exit(1);
1865
+ }
1866
+ output(await email.setDefaultIdentity(client, {
1867
+ email: identityEmail,
1868
+ displayName: flags["display-name"],
1869
+ }));
1870
+ break;
1871
+ }
1872
+ case "remove":
1873
+ case "delete": {
1874
+ const identityEmail = rest[0];
1875
+ if (!identityEmail) {
1876
+ console.error("Usage: ascendkit email-identity remove <email>");
1877
+ process.exit(1);
1878
+ }
1879
+ output(await email.removeIdentity(client, identityEmail));
1880
+ break;
1881
+ }
1882
+ case "test": {
1883
+ const identityEmail = rest[0];
1884
+ if (!identityEmail || !flags.to) {
1885
+ console.error("Usage: ascendkit email-identity test <email> --to <recipient>");
1886
+ process.exit(1);
1887
+ }
1888
+ output(await email.sendTestEmail(client, {
1889
+ to: flags.to,
1890
+ fromIdentityEmail: identityEmail,
1891
+ }));
1892
+ break;
1893
+ }
1194
1894
  default:
1195
- console.error(`Unknown email command: ${action}`);
1895
+ console.error(`Unknown email-identity command: ${action}`);
1196
1896
  process.exit(1);
1197
1897
  }
1198
1898
  }
@@ -1221,28 +1921,61 @@ async function printEmailSetup(settings) {
1221
1921
  }
1222
1922
  }
1223
1923
  }
1224
- function printIdentityStatus(settings) {
1225
- const mode = settings.domain ? "customer-owned" : "ascendkit-default";
1226
- const from = settings.fromEmail || "noreply@ascendkit.dev";
1227
- const status = settings.verificationStatus || "none";
1228
- console.log(`Identity mode: ${mode}`);
1229
- console.log(`From email: ${from}`);
1230
- console.log(`From name: ${settings.fromName || "not set"}`);
1231
- if (settings.domain) {
1232
- console.log(`Domain: ${settings.domain} (${status})`);
1233
- if (status !== "verified") {
1234
- console.log("Next step: add DNS records and run `ascendkit email domain-status --watch`");
1924
+ function printEmailSettingsSummary(settings) {
1925
+ const identities = Array.isArray(settings.identities) ? settings.identities : [];
1926
+ if (!settings.domain) {
1927
+ console.log("Sender: AscendKit default");
1928
+ console.log(`From: ${settings.fromEmail || "noreply@ascendkit.dev"}`);
1929
+ console.log(`Display name: ${settings.fromName || "not set"}`);
1930
+ console.log("Domain: not configured");
1931
+ if (identities.length > 0) {
1932
+ console.log("");
1933
+ printEmailIdentities(identities);
1235
1934
  }
1935
+ return;
1236
1936
  }
1237
- else {
1238
- console.log("Next step: run `ascendkit email use-custom <domain>` to configure customer-owned identity.");
1937
+ console.log(`Domain: ${settings.domain} (${settings.verificationStatus || "unknown"})`);
1938
+ console.log("");
1939
+ printEmailIdentities(identities);
1940
+ }
1941
+ function printEmailStatusSummary(settings, domainStatus, dnsCheck, identities) {
1942
+ if (!settings.domain) {
1943
+ console.log("Domain: not configured");
1944
+ console.log("Sender: AscendKit default");
1945
+ if (identities.length > 0) {
1946
+ console.log("");
1947
+ printEmailIdentities(identities);
1948
+ }
1949
+ return;
1239
1950
  }
1951
+ console.log(`Domain: ${settings.domain} (${domainStatus.status || settings.verificationStatus || "unknown"})`);
1952
+ if (dnsCheck?.summary) {
1953
+ console.log(`DNS: ${dnsCheck.summary.found}/${dnsCheck.summary.total} verified`);
1954
+ }
1955
+ console.log("");
1956
+ printEmailIdentities(identities);
1957
+ }
1958
+ function printEmailIdentities(identities) {
1959
+ table(identities.map((identity) => ({
1960
+ email: identity.email,
1961
+ displayName: identity.displayName || "—",
1962
+ verificationStatus: identity.verificationStatus,
1963
+ isDefault: identity.isDefault ? "yes" : "",
1964
+ })), [
1965
+ { key: "email", label: "Email", width: 36 },
1966
+ { key: "displayName", label: "Display Name", width: 24 },
1967
+ { key: "verificationStatus", label: "Status", width: 12 },
1968
+ { key: "isDefault", label: "Default", width: 7 },
1969
+ ]);
1240
1970
  }
1241
1971
  async function watchDomainStatus(client, intervalSeconds) {
1242
1972
  while (true) {
1243
- const data = await email.checkDomainStatus(client);
1244
- output(data);
1245
- if (data?.status === "verified" || data?.status === "failed" || data?.status === "none") {
1973
+ const settings = await email.getSettings(client);
1974
+ const domainStatus = await email.checkDomainStatus(client);
1975
+ const dnsCheck = settings.domain ? await email.checkDnsRecords(client) : null;
1976
+ const identities = await email.listIdentities(client);
1977
+ printEmailStatusSummary(settings, domainStatus, dnsCheck, identities.identities ?? []);
1978
+ if (domainStatus?.status === "verified" || domainStatus?.status === "failed" || domainStatus?.status === "none") {
1246
1979
  return;
1247
1980
  }
1248
1981
  await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));