@ascendkit/cli 0.1.10 → 0.1.11

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.
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * REST client for AscendKit API.
3
3
  */
4
+ export declare const CLI_VERSION: string;
4
5
  export declare class AscendKitClient {
5
6
  private baseUrl;
6
7
  private publicKey;
7
8
  private platformToken;
9
+ private upgradeWarned;
8
10
  constructor(options?: {
9
11
  apiUrl?: string;
10
12
  publicKey?: string;
@@ -14,10 +16,12 @@ export declare class AscendKitClient {
14
16
  get platformConfigured(): boolean;
15
17
  configure(publicKey: string, apiUrl?: string): void;
16
18
  configurePlatform(token: string, apiUrl?: string): void;
19
+ private get versionHeader();
17
20
  private get headers();
18
21
  private get platformHeaders();
19
22
  /** Headers with both public key and Bearer token for management write operations. */
20
23
  private get managedHeaders();
24
+ private checkUpgradeHeader;
21
25
  request<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
22
26
  platformRequest<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
23
27
  /** Unauthenticated request (for login). */
@@ -1,12 +1,29 @@
1
1
  /**
2
2
  * REST client for AscendKit API.
3
3
  */
4
+ import { readFileSync } from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, resolve } from "path";
7
+ import { DEFAULT_API_URL } from "../constants.js";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ function readPackageVersion() {
11
+ try {
12
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, "../../package.json"), "utf-8"));
13
+ return pkg.version ?? "0.0.0";
14
+ }
15
+ catch {
16
+ return "0.0.0";
17
+ }
18
+ }
19
+ export const CLI_VERSION = readPackageVersion();
4
20
  export class AscendKitClient {
5
21
  baseUrl;
6
22
  publicKey;
7
23
  platformToken;
24
+ upgradeWarned = false;
8
25
  constructor(options) {
9
- this.baseUrl = options?.apiUrl ?? "https://api.ascendkit.com";
26
+ this.baseUrl = options?.apiUrl ?? DEFAULT_API_URL;
10
27
  this.publicKey = options?.publicKey ?? null;
11
28
  this.platformToken = null;
12
29
  }
@@ -29,6 +46,9 @@ export class AscendKitClient {
29
46
  if (apiUrl)
30
47
  this.baseUrl = apiUrl;
31
48
  }
49
+ get versionHeader() {
50
+ return { "X-AscendKit-Client-Version": `cli/${CLI_VERSION}` };
51
+ }
32
52
  get headers() {
33
53
  if (!this.publicKey) {
34
54
  throw new Error("Not configured. Call ascendkit_configure with your public key first.");
@@ -36,6 +56,7 @@ export class AscendKitClient {
36
56
  return {
37
57
  "Content-Type": "application/json",
38
58
  "X-AscendKit-Public-Key": this.publicKey,
59
+ ...this.versionHeader,
39
60
  };
40
61
  }
41
62
  get platformHeaders() {
@@ -45,6 +66,7 @@ export class AscendKitClient {
45
66
  return {
46
67
  "Content-Type": "application/json",
47
68
  "Authorization": `Bearer ${this.platformToken}`,
69
+ ...this.versionHeader,
48
70
  };
49
71
  }
50
72
  /** Headers with both public key and Bearer token for management write operations. */
@@ -59,8 +81,18 @@ export class AscendKitClient {
59
81
  "Content-Type": "application/json",
60
82
  "X-AscendKit-Public-Key": this.publicKey,
61
83
  "Authorization": `Bearer ${this.platformToken}`,
84
+ ...this.versionHeader,
62
85
  };
63
86
  }
87
+ checkUpgradeHeader(response) {
88
+ const upgrade = response.headers.get("X-AscendKit-Upgrade");
89
+ if (upgrade === "recommended" && !this.upgradeWarned) {
90
+ const latest = response.headers.get("X-AscendKit-Latest-Version") ?? "latest";
91
+ console.error(`[ascendkit] A newer CLI version (v${latest}) is available. ` +
92
+ `You are running v${CLI_VERSION}. Run "npm update -g @ascendkit/cli" to upgrade.`);
93
+ this.upgradeWarned = true;
94
+ }
95
+ }
64
96
  async request(method, path, body) {
65
97
  const url = `${this.baseUrl}${path}`;
66
98
  const init = {
@@ -71,6 +103,7 @@ export class AscendKitClient {
71
103
  init.body = JSON.stringify(body);
72
104
  }
73
105
  const response = await fetch(url, init);
106
+ this.checkUpgradeHeader(response);
74
107
  if (!response.ok) {
75
108
  const text = await response.text();
76
109
  throw new Error(`AscendKit API error ${response.status}: ${text}`);
@@ -88,6 +121,7 @@ export class AscendKitClient {
88
121
  init.body = JSON.stringify(body);
89
122
  }
90
123
  const response = await fetch(url, init);
124
+ this.checkUpgradeHeader(response);
91
125
  if (!response.ok) {
92
126
  const text = await response.text();
93
127
  throw new Error(`AscendKit API error ${response.status}: ${text}`);
@@ -100,12 +134,13 @@ export class AscendKitClient {
100
134
  const url = `${this.baseUrl}${path}`;
101
135
  const init = {
102
136
  method,
103
- headers: { "Content-Type": "application/json" },
137
+ headers: { "Content-Type": "application/json", ...this.versionHeader },
104
138
  };
105
139
  if (body !== undefined) {
106
140
  init.body = JSON.stringify(body);
107
141
  }
108
142
  const response = await fetch(url, init);
143
+ this.checkUpgradeHeader(response);
109
144
  if (!response.ok) {
110
145
  const text = await response.text();
111
146
  throw new Error(`AscendKit API error ${response.status}: ${text}`);
@@ -136,6 +171,7 @@ export class AscendKitClient {
136
171
  init.body = JSON.stringify(body);
137
172
  }
138
173
  const response = await fetch(url, init);
174
+ this.checkUpgradeHeader(response);
139
175
  if (!response.ok) {
140
176
  const text = await response.text();
141
177
  throw new Error(`AscendKit API error ${response.status}: ${text}`);
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, writeFileSync } from "fs";
3
+ import { createRequire } from "module";
3
4
  import { AscendKitClient } from "./api/client.js";
5
+ import { DEFAULT_API_URL } from "./constants.js";
4
6
  import { loadAuth, loadEnvContext } from "./utils/credentials.js";
5
7
  import * as auth from "./commands/auth.js";
6
8
  import * as content from "./commands/content.js";
@@ -8,154 +10,36 @@ import * as surveys from "./commands/surveys.js";
8
10
  import * as platform from "./commands/platform.js";
9
11
  import * as journeys from "./commands/journeys.js";
10
12
  import * as email from "./commands/email.js";
13
+ import * as webhooks from "./commands/webhooks.js";
11
14
  import { parseDelay } from "./utils/duration.js";
12
- const HELP = `ascendkit - AscendKit CLI
15
+ const require = createRequire(import.meta.url);
16
+ const { version: CLI_VERSION } = require("../package.json");
17
+ const HELP = `ascendkit v${CLI_VERSION} - AscendKit CLI
13
18
 
14
19
  Usage: ascendkit <command> [options]
15
- ascendkit help [section]
20
+ ascendkit help <section>
16
21
 
17
- Platform Commands:
18
- init Initialize AscendKit in this directory (login via browser)
19
- --backend <url> API URL (default: https://api.ascendkit.com)
20
- --portal <url> Portal URL (default: http://localhost:3000)
21
- logout Clear local credentials
22
- set-env <public-key> Set active environment by public key
23
- projects list List your projects
24
- env list --project <project-id> List environments for a project
25
- env use <tier> --project <project-id> Set active environment by tier
22
+ Getting Started:
23
+ init Initialize AscendKit in this directory (login via browser)
24
+ set-env <key> Set active environment by public key
25
+ status Show current login and environment state
26
+ logout Clear local credentials
26
27
 
27
- Service Commands (require 'ascendkit init' AND 'ascendkit set-env'):
28
- auth settings Get auth settings
29
- auth settings update Update auth settings
30
- --providers <p1,p2,...> Comma-separated provider list
31
- --email-verification <true|false> Toggle email verification
32
- --waitlist <true|false> Toggle waitlist
33
- --password-reset <true|false> Toggle password reset
34
- --session-duration <duration> e.g. "7d", "24h"
35
- auth providers <p1,p2,...> Set enabled providers
36
- auth oauth <provider> Open browser to configure OAuth credentials
37
- auth oauth set <provider> Set OAuth credentials from CLI
38
- --client-id <id>
39
- --client-secret <secret> (not recommended; appears in shell history)
40
- --client-secret-stdin Read secret from stdin
41
- --callback-url <url> Auth callback URL for this provider
42
- auth users List project users
28
+ Services:
29
+ auth Authentication, providers, OAuth, users
30
+ templates Email templates and versioning
31
+ survey Surveys, questions, distribution, analytics
32
+ journey Lifecycle journeys, nodes, transitions
33
+ email Email settings, domain verification, DNS
34
+ webhook Webhook endpoints and testing
43
35
 
44
- content create Create a content template
45
- --name <name>
46
- --subject <subject>
47
- --body-html <html>
48
- --body-text <text>
49
- --slug <slug> URL-safe identifier
50
- --description <description> Template description
51
- content list List all templates
52
- --query <search> Search by name/slug/description
53
- --system true Show only system templates
54
- --custom true Show only custom templates
55
- content get <template-id> Get a template
56
- content update <template-id> Update a template (creates new version)
57
- --subject <subject>
58
- --body-html <html>
59
- --body-text <text>
60
- --change-note <note>
61
- content delete <template-id> Delete a template
62
- content versions <template-id> List template versions
63
- content version <template-id> <n> Get specific version
36
+ Project Management:
37
+ projects List and create projects
38
+ env List, switch, and promote environments
39
+ verify Check all services in the active environment
64
40
 
65
- survey create Create a survey
66
- --name <name>
67
- --type <nps|csat|custom>
68
- --definition <json>
69
- survey list List all surveys
70
- survey get <survey-id> Get a survey
71
- survey update <survey-id> Update a survey
72
- --name <name>
73
- --status <draft|active|paused>
74
- --definition <json>
75
- survey delete <survey-id> Delete a survey
76
- survey distribute <survey-id> Distribute to users
77
- --users <usr_id1,usr_id2,...>
78
- survey invitations <survey-id> List invitations
79
- survey analytics <survey-id> Get analytics
80
- survey export-definition <survey-id> Export survey definition JSON
81
- --out <file> Optional output file path (prints to stdout if omitted)
82
- survey import-definition <survey-id> Import survey definition JSON (definition only; slug unchanged)
83
- --in <file> Input JSON file (accepts raw definition or {definition: ...})
84
-
85
- journey create Create a journey
86
- --name <name>
87
- --entry-event <event> e.g. "user.created"
88
- --entry-node <node> First node name
89
- --nodes <json> Node definitions (JSON, optional)
90
- --transitions <json> Transition definitions (JSON)
91
- --description <description>
92
- --entry-conditions <json> Filter on entry event payload
93
- --re-entry-policy <skip|restart> Default: skip
94
- journey list List all journeys
95
- --status <draft|active|paused|archived>
96
- journey get <journey-id> Get a journey
97
- journey update <journey-id> Update a journey
98
- --name <name>
99
- --nodes <json>
100
- --transitions <json>
101
- --description <description>
102
- --entry-event <event>
103
- --entry-node <node>
104
- --entry-conditions <json>
105
- --re-entry-policy <skip|restart>
106
- journey delete <journey-id> Delete a journey
107
- journey activate <journey-id> Activate a draft journey
108
- journey pause <journey-id> Pause an active journey
109
- journey archive <journey-id> Archive a journey (permanent)
110
- journey analytics <journey-id> Get journey analytics
111
- journey list-nodes <journey-id> List nodes with action and transition counts
112
- journey add-node <journey-id> Add a node
113
- --name <node-name>
114
- --action <json> Optional action JSON (default: {"type":"none"})
115
- --terminal <true|false> Optional terminal flag (default: false)
116
- journey edit-node <journey-id> <node-name>
117
- --action <json> Optional action JSON
118
- --terminal <true|false> Optional terminal flag
119
- journey remove-node <journey-id> <node-name>
120
- journey list-transitions <journey-id>
121
- --from <node-name> Optional source node filter
122
- --to <node-name> Optional target node filter
123
- journey add-transition <journey-id>
124
- --from <node-name>
125
- --to <node-name>
126
- --trigger <json> Trigger JSON, e.g. {"type":"event","event":"user.login"}
127
- --priority <n> Optional numeric priority
128
- --name <transition-name> Optional transition name
129
- journey edit-transition <journey-id> <transition-name>
130
- --trigger <json> Optional trigger JSON
131
- --priority <n> Optional numeric priority
132
- journey remove-transition <journey-id> <transition-name>
133
-
134
- email settings Get email settings
135
- email settings update Update email settings
136
- --from-email <email> Sender email address
137
- --from-name <name> Sender display name
138
- email identity Show current identity mode and next steps
139
- email use-default Switch to AscendKit default sender
140
- email use-custom <domain> Switch to customer-owned identity (starts DNS setup)
141
- --from-email <email> Optional sender after domain setup
142
- --from-name <name> Optional display name after domain setup
143
- email setup-domain <domain> Start domain verification
144
- email domain-status Check domain verification status
145
- --watch Poll until verified/failed
146
- --interval <seconds> Poll interval for --watch (default: 15)
147
- email open-dns Show DNS provider URL for current domain
148
- --domain <domain> Optional override domain
149
- --open Open URL in browser
150
- email remove-domain Remove domain and reset to default
151
-
152
- status Show current login and environment state
153
- verify Check all services in the active environment
154
-
155
- Environment:
156
- ASCENDKIT_PUBLIC_KEY Environment public key (overrides stored credentials)
157
- ASCENDKIT_API_URL API URL (default: https://api.ascendkit.com)
158
- `;
41
+ Run "ascendkit help <section>" for detailed command usage.
42
+ e.g. ascendkit help auth, ascendkit help journey`;
159
43
  const HELP_SECTION = {
160
44
  auth: `Usage: ascendkit auth <command>
161
45
 
@@ -166,16 +50,16 @@ Commands:
166
50
  auth oauth <provider>
167
51
  auth oauth set <provider> --client-id <id> [--client-secret <secret> | --client-secret-stdin] [--callback-url <url>]
168
52
  auth users`,
169
- content: `Usage: ascendkit content <command>
53
+ templates: `Usage: ascendkit templates <command>
170
54
 
171
55
  Commands:
172
- content create --name <name> --subject <subject> --body-html <html> --body-text <text> [--slug <slug>] [--description <description>]
173
- content list [--query <search>] [--system true|--custom true]
174
- content get <template-id>
175
- content update <template-id> [--subject <subject>] [--body-html <html>] [--body-text <text>] [--change-note <note>]
176
- content delete <template-id>
177
- content versions <template-id>
178
- content version <template-id> <n>`,
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>`,
179
63
  survey: `Usage: ascendkit survey <command>
180
64
 
181
65
  Commands:
@@ -191,7 +75,6 @@ Commands:
191
75
  survey import-definition <survey-id> --in <file>
192
76
 
193
77
  Notes:
194
- - Question-level editing commands are available via MCP tools, not this terminal CLI.
195
78
  - import-definition only updates the survey definition. It does not mutate slug/name/status.`,
196
79
  journey: `Usage: ascendkit journey <command>
197
80
 
@@ -225,17 +108,33 @@ Commands:
225
108
  email domain-status [--watch] [--interval <seconds>]
226
109
  email open-dns [--domain <domain>] [--open]
227
110
  email remove-domain`,
111
+ webhook: `Usage: ascendkit webhook <command>
112
+
113
+ Commands:
114
+ webhook create --url <url> [--events <e1,e2,...>]
115
+ webhook list
116
+ webhook get <webhook-id>
117
+ webhook update <webhook-id> [--url <url>] [--events <e1,e2,...>] [--status <active|inactive>]
118
+ webhook delete <webhook-id>
119
+ webhook test <webhook-id> [--event <event-type>]`,
228
120
  env: `Usage: ascendkit env <command>
229
121
 
230
122
  Commands:
231
123
  env list --project <project-id>
232
- env use <tier> --project <project-id>`,
233
- projects: `Usage: ascendkit projects list`,
124
+ env use <tier> --project <project-id>
125
+ env promote <env-id> --target <tier>`,
126
+ projects: `Usage: ascendkit projects <command>
127
+
128
+ Commands:
129
+ projects list
130
+ projects create --name <name> [--description <description>] [--services <s1,s2,...>]`,
234
131
  };
235
132
  function printSectionHelp(section) {
236
133
  if (!section)
237
134
  return false;
238
- const key = section.toLowerCase();
135
+ let key = section.toLowerCase();
136
+ if (key === "content")
137
+ key = "templates";
239
138
  const text = HELP_SECTION[key];
240
139
  if (!text)
241
140
  return false;
@@ -263,7 +162,7 @@ function getClient() {
263
162
  process.exit(1);
264
163
  }
265
164
  const client = new AscendKitClient({
266
- apiUrl: apiUrl ?? "https://api.ascendkit.com",
165
+ apiUrl: apiUrl ?? DEFAULT_API_URL,
267
166
  publicKey,
268
167
  });
269
168
  if (auth?.token) {
@@ -333,6 +232,10 @@ function table(rows, columns) {
333
232
  }
334
233
  async function run() {
335
234
  const args = process.argv.slice(2);
235
+ if (args[0] === "--version" || args[0] === "-v" || args[0] === "-V") {
236
+ console.log(CLI_VERSION);
237
+ return;
238
+ }
336
239
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
337
240
  console.log(HELP);
338
241
  return;
@@ -381,8 +284,34 @@ async function run() {
381
284
  { key: "enabledServices", label: "Services", width: 30 },
382
285
  ]);
383
286
  }
287
+ else if (action === "create") {
288
+ const flags = parseFlags(args.slice(2));
289
+ if (!flags.name) {
290
+ console.error("Usage: ascendkit projects create --name <name> [--description <description>] [--services <s1,s2,...>]");
291
+ process.exit(1);
292
+ }
293
+ try {
294
+ output(await platform.createProject(flags.name, flags.description, flags.services?.split(",")));
295
+ }
296
+ catch (err) {
297
+ let message = err instanceof Error ? err.message : String(err);
298
+ const jsonMatch = message.match(/\{.*\}/s);
299
+ if (jsonMatch) {
300
+ try {
301
+ const parsed = JSON.parse(jsonMatch[0]);
302
+ if (parsed.error)
303
+ message = parsed.error;
304
+ else if (parsed.detail)
305
+ message = parsed.detail;
306
+ }
307
+ catch { /* use raw message */ }
308
+ }
309
+ console.error(message);
310
+ process.exit(1);
311
+ }
312
+ }
384
313
  else {
385
- console.error('Usage: ascendkit projects list');
314
+ console.error('Usage: ascendkit projects list|create');
386
315
  process.exit(1);
387
316
  }
388
317
  return;
@@ -409,6 +338,7 @@ async function run() {
409
338
  case "auth":
410
339
  await runAuth(client, action, args.slice(2));
411
340
  break;
341
+ case "templates":
412
342
  case "content":
413
343
  await runContent(client, action, args.slice(2));
414
344
  break;
@@ -421,6 +351,9 @@ async function run() {
421
351
  case "email":
422
352
  await runEmail(client, action, args.slice(2));
423
353
  break;
354
+ case "webhook":
355
+ await runWebhook(client, action, args.slice(2));
356
+ break;
424
357
  default:
425
358
  console.error(`Unknown command: ${domain}`);
426
359
  console.error('Run "ascendkit --help" for usage');
@@ -448,9 +381,39 @@ async function runEnv(action, rest) {
448
381
  }
449
382
  await platform.useEnvironment(rest[0], flags.project);
450
383
  break;
384
+ case "promote": {
385
+ const envId = rest[0];
386
+ const target = flags.target;
387
+ if (!envId || !target) {
388
+ console.error("Usage: ascendkit env promote <env-id> --target <tier>");
389
+ process.exit(1);
390
+ }
391
+ try {
392
+ const result = await platform.promoteEnvironment(envId, target);
393
+ console.log("Promotion successful:");
394
+ console.log(JSON.stringify(result, null, 2));
395
+ }
396
+ catch (err) {
397
+ let message = err instanceof Error ? err.message : String(err);
398
+ const jsonMatch = message.match(/\{.*\}/s);
399
+ if (jsonMatch) {
400
+ try {
401
+ const parsed = JSON.parse(jsonMatch[0]);
402
+ if (parsed.error)
403
+ message = parsed.error;
404
+ else if (parsed.detail)
405
+ message = parsed.detail;
406
+ }
407
+ catch { /* use raw message */ }
408
+ }
409
+ console.error(message);
410
+ process.exit(1);
411
+ }
412
+ break;
413
+ }
451
414
  default:
452
415
  console.error(`Unknown env command: ${action}`);
453
- console.error("Usage: ascendkit env list|use");
416
+ console.error("Usage: ascendkit env list|use|promote");
454
417
  process.exit(1);
455
418
  }
456
419
  }
@@ -541,7 +504,7 @@ async function runContent(client, action, rest) {
541
504
  switch (action) {
542
505
  case "create":
543
506
  if (!flags.name || !flags.subject || !flags["body-html"] || !flags["body-text"]) {
544
- console.error("Usage: ascendkit content create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>]");
507
+ console.error("Usage: ascendkit templates create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>]");
545
508
  process.exit(1);
546
509
  }
547
510
  output(await content.createTemplate(client, {
@@ -569,14 +532,14 @@ async function runContent(client, action, rest) {
569
532
  }
570
533
  case "get":
571
534
  if (!rest[0]) {
572
- console.error("Usage: ascendkit content get <template-id>");
535
+ console.error("Usage: ascendkit templates get <template-id>");
573
536
  process.exit(1);
574
537
  }
575
538
  output(await content.getTemplate(client, rest[0]));
576
539
  break;
577
540
  case "update":
578
541
  if (!rest[0]) {
579
- console.error("Usage: ascendkit content update <template-id> [--flags]");
542
+ console.error("Usage: ascendkit templates update <template-id> [--flags]");
580
543
  process.exit(1);
581
544
  }
582
545
  output(await content.updateTemplate(client, rest[0], {
@@ -588,27 +551,27 @@ async function runContent(client, action, rest) {
588
551
  break;
589
552
  case "delete":
590
553
  if (!rest[0]) {
591
- console.error("Usage: ascendkit content delete <template-id>");
554
+ console.error("Usage: ascendkit templates delete <template-id>");
592
555
  process.exit(1);
593
556
  }
594
557
  output(await content.deleteTemplate(client, rest[0]));
595
558
  break;
596
559
  case "versions":
597
560
  if (!rest[0]) {
598
- console.error("Usage: ascendkit content versions <template-id>");
561
+ console.error("Usage: ascendkit templates versions <template-id>");
599
562
  process.exit(1);
600
563
  }
601
564
  output(await content.listVersions(client, rest[0]));
602
565
  break;
603
566
  case "version":
604
567
  if (!rest[0] || !rest[1]) {
605
- console.error("Usage: ascendkit content version <template-id> <n>");
568
+ console.error("Usage: ascendkit templates version <template-id> <n>");
606
569
  process.exit(1);
607
570
  }
608
571
  output(await content.getVersion(client, rest[0], parseInt(rest[1], 10)));
609
572
  break;
610
573
  default:
611
- console.error(`Unknown content command: ${action}`);
574
+ console.error(`Unknown templates command: ${action}`);
612
575
  process.exit(1);
613
576
  }
614
577
  }
@@ -721,6 +684,59 @@ async function runSurvey(client, action, rest) {
721
684
  }));
722
685
  break;
723
686
  }
687
+ case "list-questions":
688
+ if (!rest[0]) {
689
+ console.error("Usage: ascendkit survey list-questions <survey-id>");
690
+ process.exit(1);
691
+ }
692
+ output(await surveys.listQuestions(client, rest[0]));
693
+ break;
694
+ case "add-question": {
695
+ if (!rest[0] || !flags.type || !flags.title) {
696
+ console.error("Usage: ascendkit survey add-question <survey-id> --type <type> --title <title> [--name <name>] [--required <true|false>] [--choices <c1,c2,...>] [--position <n>]");
697
+ process.exit(1);
698
+ }
699
+ const params = { type: flags.type, title: flags.title };
700
+ if (flags.name)
701
+ params.name = flags.name;
702
+ if (flags.required)
703
+ params.isRequired = flags.required === "true";
704
+ if (flags.choices)
705
+ params.choices = flags.choices.split(",");
706
+ if (flags.position != null)
707
+ params.position = Number(flags.position);
708
+ output(await surveys.addQuestion(client, rest[0], params));
709
+ break;
710
+ }
711
+ case "edit-question": {
712
+ if (!rest[0] || !rest[1]) {
713
+ console.error("Usage: ascendkit survey edit-question <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]");
714
+ process.exit(1);
715
+ }
716
+ const params = {};
717
+ if (flags.title)
718
+ params.title = flags.title;
719
+ if (flags.required)
720
+ params.isRequired = flags.required === "true";
721
+ if (flags.choices)
722
+ params.choices = flags.choices.split(",");
723
+ output(await surveys.editQuestion(client, rest[0], rest[1], params));
724
+ break;
725
+ }
726
+ case "remove-question":
727
+ if (!rest[0] || !rest[1]) {
728
+ console.error("Usage: ascendkit survey remove-question <survey-id> <question-name>");
729
+ process.exit(1);
730
+ }
731
+ output(await surveys.removeQuestion(client, rest[0], rest[1]));
732
+ break;
733
+ case "reorder-questions":
734
+ if (!rest[0] || !flags.order) {
735
+ console.error("Usage: ascendkit survey reorder-questions <survey-id> --order <name1,name2,...>");
736
+ process.exit(1);
737
+ }
738
+ output(await surveys.reorderQuestions(client, rest[0], flags.order.split(",")));
739
+ break;
724
740
  default:
725
741
  console.error(`Unknown survey command: ${action}`);
726
742
  console.error('Run "ascendkit survey --help" for usage');
@@ -1009,6 +1025,65 @@ async function runJourney(client, action, rest) {
1009
1025
  process.exit(1);
1010
1026
  }
1011
1027
  }
1028
+ async function runWebhook(client, action, rest) {
1029
+ const flags = parseFlags(rest);
1030
+ switch (action) {
1031
+ case "create":
1032
+ if (!flags.url) {
1033
+ console.error("Usage: ascendkit webhook create --url <url> [--events <e1,e2,...>]");
1034
+ process.exit(1);
1035
+ }
1036
+ output(await webhooks.createWebhook(client, {
1037
+ url: flags.url,
1038
+ events: flags.events ? flags.events.split(",") : undefined,
1039
+ }));
1040
+ break;
1041
+ case "list":
1042
+ table(await webhooks.listWebhooks(client), [
1043
+ { key: "id", label: "ID" },
1044
+ { key: "url", label: "URL", width: 40 },
1045
+ { key: "status", label: "Status" },
1046
+ { key: "events", label: "Events", width: 30 },
1047
+ ]);
1048
+ break;
1049
+ case "get":
1050
+ if (!rest[0]) {
1051
+ console.error("Usage: ascendkit webhook get <webhook-id>");
1052
+ process.exit(1);
1053
+ }
1054
+ output(await webhooks.getWebhook(client, rest[0]));
1055
+ break;
1056
+ case "update":
1057
+ if (!rest[0]) {
1058
+ console.error("Usage: ascendkit webhook update <webhook-id> [--flags]");
1059
+ process.exit(1);
1060
+ }
1061
+ output(await webhooks.updateWebhook(client, rest[0], {
1062
+ url: flags.url,
1063
+ events: flags.events ? flags.events.split(",") : undefined,
1064
+ status: flags.status,
1065
+ }));
1066
+ break;
1067
+ case "delete":
1068
+ if (!rest[0]) {
1069
+ console.error("Usage: ascendkit webhook delete <webhook-id>");
1070
+ process.exit(1);
1071
+ }
1072
+ output(await webhooks.deleteWebhook(client, rest[0]));
1073
+ break;
1074
+ case "test":
1075
+ if (!rest[0]) {
1076
+ console.error("Usage: ascendkit webhook test <webhook-id> [--event <event-type>]");
1077
+ process.exit(1);
1078
+ }
1079
+ output(await webhooks.testWebhook(client, rest[0], flags.event));
1080
+ break;
1081
+ default:
1082
+ console.error(`Unknown webhook command: ${action}`);
1083
+ console.error('Run "ascendkit webhook --help" for usage');
1084
+ process.exit(1);
1085
+ }
1086
+ }
1012
1087
  async function runEmail(client, action, rest) {
1013
1088
  const flags = parseFlags(rest);
1014
1089
  switch (action) {
@@ -1,6 +1,7 @@
1
1
  export declare function init(apiUrl?: string, portalUrl?: string): Promise<void>;
2
2
  export declare function logout(): void;
3
3
  export declare function listProjects(): Promise<unknown>;
4
+ export declare function createProject(name: string, description?: string, enabledServices?: string[]): Promise<unknown>;
4
5
  export declare function listEnvironments(projectId: string): Promise<unknown>;
5
6
  export declare function useEnvironment(tier: string, projectId: string): Promise<void>;
6
7
  export declare function setEnv(publicKey: string): Promise<void>;
@@ -33,3 +34,8 @@ export declare function mcpListProjects(client: AscendKitClient): Promise<unknow
33
34
  export declare function mcpCreateProject(client: AscendKitClient, params: McpCreateProjectParams): Promise<unknown>;
34
35
  export declare function mcpListEnvironments(client: AscendKitClient, projectId: string): Promise<unknown>;
35
36
  export declare function mcpCreateEnvironment(client: AscendKitClient, params: McpCreateEnvironmentParams): Promise<unknown>;
37
+ export declare function promoteEnvironment(environmentId: string, targetTier: string): Promise<unknown>;
38
+ export declare function mcpPromoteEnvironment(client: AscendKitClient, params: {
39
+ environmentId: string;
40
+ targetTier: string;
41
+ }): Promise<unknown>;
@@ -2,8 +2,7 @@ import { hostname, platform as osPlatform, release } from "node:os";
2
2
  import { readdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { loadAuth, saveAuth, deleteAuth, saveEnvContext, ensureGitignore } from "../utils/credentials.js";
5
- const DEFAULT_API_URL = "https://api.ascendkit.com";
6
- const DEFAULT_PORTAL_URL = "http://localhost:3000";
5
+ import { DEFAULT_API_URL, DEFAULT_PORTAL_URL } from "../constants.js";
7
6
  const POLL_INTERVAL_MS = 2000;
8
7
  const DEVICE_CODE_EXPIRY_MS = 300_000; // 5 minutes
9
8
  const IGNORE_DIRS = new Set([".git", "node_modules", ".next", "dist", "build"]);
@@ -66,9 +65,8 @@ async function promptChoice(question, options) {
66
65
  rl.close();
67
66
  }
68
67
  }
69
- async function findEnvFiles(root) {
70
- const examples = [];
71
- const runtime = [];
68
+ async function findEnvFolders(root) {
69
+ const byDir = new Map();
72
70
  async function walk(dir) {
73
71
  const entries = await readdir(dir, { withFileTypes: true });
74
72
  for (const entry of entries) {
@@ -81,18 +79,34 @@ async function findEnvFiles(root) {
81
79
  }
82
80
  if (!entry.isFile())
83
81
  continue;
84
- if (entry.name === ".env.example") {
85
- examples.push(fullPath);
86
- }
87
- else if (entry.name === ".env" || entry.name === ".env.local") {
88
- runtime.push(fullPath);
82
+ const isExample = entry.name === ".env.example";
83
+ const isRuntime = entry.name === ".env" || entry.name === ".env.local";
84
+ if (!isExample && !isRuntime)
85
+ continue;
86
+ let bucket = byDir.get(dir);
87
+ if (!bucket) {
88
+ bucket = { examples: [], runtime: [] };
89
+ byDir.set(dir, bucket);
89
90
  }
91
+ if (isExample)
92
+ bucket.examples.push(fullPath);
93
+ else
94
+ bucket.runtime.push(fullPath);
90
95
  }
91
96
  }
92
97
  await walk(root);
93
- examples.sort();
94
- runtime.sort();
95
- return { examples, runtime };
98
+ const folders = [];
99
+ for (const [dir, bucket] of byDir) {
100
+ const rel = path.relative(root, dir) || ".";
101
+ folders.push({
102
+ dir,
103
+ label: rel === "." ? "(project root)" : `${rel}/`,
104
+ examples: bucket.examples.sort(),
105
+ runtime: bucket.runtime.sort(),
106
+ });
107
+ }
108
+ folders.sort((a, b) => a.dir.localeCompare(b.dir));
109
+ return folders;
96
110
  }
97
111
  function readEnvValue(content, key) {
98
112
  const match = content.match(new RegExp(`^\\s*${key}=(.*)$`, "m"));
@@ -263,28 +277,26 @@ export async function init(apiUrl, portalUrl) {
263
277
  console.log(`\nInitialized as ${result.email ?? "unknown"}`);
264
278
  // Seed .env files with API URL and empty key placeholders
265
279
  const cwd = process.cwd();
266
- const discovered = await findEnvFiles(cwd);
267
- if (discovered.examples.length > 0) {
268
- console.log(`\nFound ${discovered.examples.length} .env.example file(s).`);
269
- const updateExamples = await promptYesNo("Update with AscendKit env placeholders?", true);
270
- if (updateExamples) {
271
- for (const filePath of discovered.examples) {
280
+ const folders = await findEnvFolders(cwd);
281
+ if (folders.length > 0) {
282
+ console.log(`\nFound env files in ${folders.length} folder(s).`);
283
+ for (const folder of folders) {
284
+ const fileNames = [...folder.examples, ...folder.runtime]
285
+ .map(f => path.basename(f)).join(", ");
286
+ const update = await promptYesNo(` ${folder.label} (${fileNames}) — update?`, true);
287
+ if (!update)
288
+ continue;
289
+ for (const filePath of folder.examples) {
272
290
  const changed = await updateEnvExampleFile(filePath);
273
291
  if (changed)
274
- console.log(` Updated ${path.relative(cwd, filePath)}`);
292
+ console.log(` Updated ${path.relative(cwd, filePath)}`);
275
293
  }
276
- }
277
- }
278
- if (discovered.runtime.length > 0) {
279
- console.log(`\nFound ${discovered.runtime.length} .env/.env.local file(s).`);
280
- const updateRuntime = await promptYesNo("Seed with AscendKit API URL?", true);
281
- if (updateRuntime) {
282
- for (const filePath of discovered.runtime) {
294
+ for (const filePath of folder.runtime) {
283
295
  const changed = await updateRuntimeEnvFile(filePath, api, "", "", {
284
296
  preserveExistingKeys: true,
285
297
  });
286
298
  if (changed)
287
- console.log(` Updated ${path.relative(cwd, filePath)}`);
299
+ console.log(` Updated ${path.relative(cwd, filePath)}`);
288
300
  }
289
301
  }
290
302
  }
@@ -318,6 +330,14 @@ export async function listProjects() {
318
330
  const auth = requireAuth();
319
331
  return apiRequest(auth.apiUrl, "GET", "/api/platform/projects", undefined, auth.token);
320
332
  }
333
+ export async function createProject(name, description, enabledServices) {
334
+ const auth = requireAuth();
335
+ return apiRequest(auth.apiUrl, "POST", "/api/platform/projects", {
336
+ name,
337
+ description: description ?? "",
338
+ enabledServices: enabledServices ?? [],
339
+ }, auth.token);
340
+ }
321
341
  export async function listEnvironments(projectId) {
322
342
  const auth = requireAuth();
323
343
  return apiRequest(auth.apiUrl, "GET", `/api/platform/projects/${projectId}/environments`, undefined, auth.token);
@@ -426,40 +446,38 @@ export async function setEnv(publicKey) {
426
446
  console.log(` → Environment: ${result.environment.name}`);
427
447
  console.log(` → Role: ${result.role}`);
428
448
  const cwd = process.cwd();
429
- const discovered = await findEnvFiles(cwd);
430
- const updatedExamples = [];
431
- const updatedRuntime = [];
432
- console.log(`\nFound ${discovered.examples.length} .env.example file(s).`);
433
- if (discovered.examples.length > 0) {
434
- const updateExamples = await promptYesNo("Update with env placeholders?", true);
435
- if (updateExamples) {
436
- for (const filePath of discovered.examples) {
449
+ const folders = await findEnvFolders(cwd);
450
+ const updatedFiles = [];
451
+ const consentedRuntimeFolders = [];
452
+ if (folders.length > 0) {
453
+ console.log(`\nFound env files in ${folders.length} folder(s).`);
454
+ for (const folder of folders) {
455
+ const fileNames = [...folder.examples, ...folder.runtime]
456
+ .map(f => path.basename(f)).join(", ");
457
+ const update = await promptYesNo(` ${folder.label} (${fileNames}) — update?`, true);
458
+ if (!update)
459
+ continue;
460
+ for (const filePath of folder.examples) {
437
461
  const changed = await updateEnvExampleFile(filePath);
438
462
  if (changed)
439
- updatedExamples.push(path.relative(cwd, filePath));
463
+ updatedFiles.push(path.relative(cwd, filePath));
440
464
  }
441
- }
442
- }
443
- console.log(`\nFound ${discovered.runtime.length} .env/.env.local file(s).`);
444
- let runtimeConsent = false;
445
- if (discovered.runtime.length > 0) {
446
- runtimeConsent = await promptYesNo("Update with actual env and secret values?", false);
447
- if (runtimeConsent) {
448
- for (const filePath of discovered.runtime) {
449
- const changed = await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey ?? "");
450
- if (changed)
451
- updatedRuntime.push(path.relative(cwd, filePath));
465
+ if (folder.runtime.length > 0) {
466
+ consentedRuntimeFolders.push(folder);
467
+ for (const filePath of folder.runtime) {
468
+ const changed = await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey ?? "");
469
+ if (changed)
470
+ updatedFiles.push(path.relative(cwd, filePath));
471
+ }
452
472
  }
453
473
  }
454
474
  }
455
475
  console.log("\nUpdated files:");
456
- if (updatedExamples.length === 0 && updatedRuntime.length === 0) {
476
+ if (updatedFiles.length === 0) {
457
477
  console.log(" (none)");
458
478
  }
459
479
  else {
460
- for (const filePath of updatedExamples)
461
- console.log(` - ${filePath}`);
462
- for (const filePath of updatedRuntime)
480
+ for (const filePath of updatedFiles)
463
481
  console.log(` - ${filePath}`);
464
482
  }
465
483
  if (!result.environment.secretKey) {
@@ -468,9 +486,11 @@ export async function setEnv(publicKey) {
468
486
  // --- Webhook setup ---
469
487
  const webhookSecret = await setupWebhook(auth, result.environment.publicKey);
470
488
  if (webhookSecret) {
471
- if (runtimeConsent && discovered.runtime.length > 0) {
472
- for (const filePath of discovered.runtime) {
473
- await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey ?? "", { preserveExistingKeys: true, webhookSecret });
489
+ if (consentedRuntimeFolders.length > 0) {
490
+ for (const folder of consentedRuntimeFolders) {
491
+ for (const filePath of folder.runtime) {
492
+ await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey ?? "", { preserveExistingKeys: true, webhookSecret });
493
+ }
474
494
  }
475
495
  console.log("\nWebhook secret written to .env file(s).");
476
496
  }
@@ -515,3 +535,10 @@ export async function mcpListEnvironments(client, projectId) {
515
535
  export async function mcpCreateEnvironment(client, params) {
516
536
  return client.platformRequest("POST", `/api/platform/projects/${params.projectId}/environments`, { name: params.name, description: params.description ?? "", tier: params.tier });
517
537
  }
538
+ export async function promoteEnvironment(environmentId, targetTier) {
539
+ const auth = requireAuth();
540
+ return apiRequest(auth.apiUrl, "POST", `/api/platform/environments/${environmentId}/promote`, { targetTier, dryRun: false }, auth.token);
541
+ }
542
+ export async function mcpPromoteEnvironment(client, params) {
543
+ return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: params.targetTier, dryRun: false });
544
+ }
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_API_URL = "https://api.ascendkit.dev";
2
+ export declare const DEFAULT_PORTAL_URL = "https://ascendkit.dev";
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_API_URL = "https://api.ascendkit.dev";
2
+ export const DEFAULT_PORTAL_URL = "https://ascendkit.dev";
package/dist/mcp.js CHANGED
@@ -10,8 +10,9 @@ import { registerPlatformTools } from "./tools/platform.js";
10
10
  import { registerEmailTools } from "./tools/email.js";
11
11
  import { registerJourneyTools } from "./tools/journeys.js";
12
12
  import { registerWebhookTools } from "./tools/webhooks.js";
13
+ import { DEFAULT_API_URL } from "./constants.js";
13
14
  const client = new AscendKitClient({
14
- apiUrl: process.env.ASCENDKIT_API_URL ?? "https://api.ascendkit.com",
15
+ apiUrl: process.env.ASCENDKIT_API_URL ?? DEFAULT_API_URL,
15
16
  });
16
17
  const server = new McpServer({
17
18
  name: "ascendkit",
@@ -19,7 +20,7 @@ const server = new McpServer({
19
20
  });
20
21
  server.tool("ascendkit_configure", "Configure AscendKit with your project's public key. Must be called before using any other AscendKit tools.", {
21
22
  publicKey: z.string().describe("Your project's public key (pk_dev_..., pk_beta_..., or pk_prod_...)"),
22
- apiUrl: z.string().optional().describe("AscendKit API URL (default: https://api.ascendkit.com)"),
23
+ apiUrl: z.string().optional().describe("AscendKit API URL (default: https://api.ascendkit.dev)"),
23
24
  }, async (params) => {
24
25
  client.configure(params.publicKey, params.apiUrl);
25
26
  return {
@@ -29,10 +29,30 @@ export function registerPlatformTools(server, client) {
29
29
  .optional()
30
30
  .describe('Services to enable, e.g. ["auth", "content", "surveys"]'),
31
31
  }, async (params) => {
32
- const data = await platform.mcpCreateProject(client, params);
33
- return {
34
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
35
- };
32
+ try {
33
+ const data = await platform.mcpCreateProject(client, params);
34
+ return {
35
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
36
+ };
37
+ }
38
+ catch (err) {
39
+ let message = err instanceof Error ? err.message : String(err);
40
+ const jsonMatch = message.match(/\{.*\}/s);
41
+ if (jsonMatch) {
42
+ try {
43
+ const parsed = JSON.parse(jsonMatch[0]);
44
+ if (parsed.error)
45
+ message = parsed.error;
46
+ else if (parsed.detail)
47
+ message = parsed.detail;
48
+ }
49
+ catch { /* use raw message */ }
50
+ }
51
+ return {
52
+ content: [{ type: "text", text: message }],
53
+ isError: true,
54
+ };
55
+ }
36
56
  });
37
57
  server.tool("platform_list_environments", "List environments for a project", {
38
58
  projectId: z.string().describe("Project ID (prj_ prefixed)"),
@@ -55,9 +75,63 @@ export function registerPlatformTools(server, client) {
55
75
  .string()
56
76
  .describe('Environment tier: "dev", "beta", or "prod"'),
57
77
  }, async (params) => {
58
- const data = await platform.mcpCreateEnvironment(client, params);
59
- return {
60
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
61
- };
78
+ try {
79
+ const data = await platform.mcpCreateEnvironment(client, params);
80
+ return {
81
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
82
+ };
83
+ }
84
+ catch (err) {
85
+ let message = err instanceof Error ? err.message : String(err);
86
+ const jsonMatch = message.match(/\{.*\}/s);
87
+ if (jsonMatch) {
88
+ try {
89
+ const parsed = JSON.parse(jsonMatch[0]);
90
+ if (parsed.error)
91
+ message = parsed.error;
92
+ else if (parsed.detail)
93
+ message = parsed.detail;
94
+ }
95
+ catch { /* use raw message */ }
96
+ }
97
+ return {
98
+ content: [{ type: "text", text: message }],
99
+ isError: true,
100
+ };
101
+ }
102
+ });
103
+ server.tool("platform_promote_environment", "Promote an environment's configuration to a higher tier (dev → beta → prod).", {
104
+ environmentId: z.string().describe("Environment ID to promote"),
105
+ targetTier: z
106
+ .string()
107
+ .describe('Target tier: "beta" or "prod"'),
108
+ }, async (params) => {
109
+ try {
110
+ const data = await platform.mcpPromoteEnvironment(client, {
111
+ environmentId: params.environmentId,
112
+ targetTier: params.targetTier,
113
+ });
114
+ return {
115
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
116
+ };
117
+ }
118
+ catch (err) {
119
+ let message = err instanceof Error ? err.message : String(err);
120
+ const jsonMatch = message.match(/\{.*\}/s);
121
+ if (jsonMatch) {
122
+ try {
123
+ const parsed = JSON.parse(jsonMatch[0]);
124
+ if (parsed.error)
125
+ message = parsed.error;
126
+ else if (parsed.detail)
127
+ message = parsed.detail;
128
+ }
129
+ catch { /* use raw message */ }
130
+ }
131
+ return {
132
+ content: [{ type: "text", text: message }],
133
+ isError: true,
134
+ };
135
+ }
62
136
  });
63
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascendkit/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "AscendKit CLI and MCP server",
5
5
  "author": "ascendkit.dev",
6
6
  "license": "MIT",
@@ -8,7 +8,14 @@
8
8
  "publishConfig": {
9
9
  "access": "public"
10
10
  },
11
- "keywords": ["ascendkit", "cli", "mcp", "model-context-protocol", "b2b", "saas"],
11
+ "keywords": [
12
+ "ascendkit",
13
+ "cli",
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "b2b",
17
+ "saas"
18
+ ],
12
19
  "type": "module",
13
20
  "main": "dist/cli.js",
14
21
  "files": [
@@ -23,8 +30,7 @@
23
30
  "scripts": {
24
31
  "build": "tsc",
25
32
  "dev": "tsc --watch",
26
- "start": "node dist/cli.js",
27
- "release:local": "./scripts/release-local.sh"
33
+ "start": "node dist/cli.js"
28
34
  },
29
35
  "dependencies": {
30
36
  "@modelcontextprotocol/sdk": "^1.0.0",