@ascendkit/cli 0.1.10

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/dist/api/client.d.ts +34 -0
  3. package/dist/api/client.js +155 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +1153 -0
  6. package/dist/commands/auth.d.ts +17 -0
  7. package/dist/commands/auth.js +29 -0
  8. package/dist/commands/content.d.ts +25 -0
  9. package/dist/commands/content.js +28 -0
  10. package/dist/commands/email.d.ts +37 -0
  11. package/dist/commands/email.js +39 -0
  12. package/dist/commands/journeys.d.ts +86 -0
  13. package/dist/commands/journeys.js +69 -0
  14. package/dist/commands/platform.d.ts +35 -0
  15. package/dist/commands/platform.js +517 -0
  16. package/dist/commands/surveys.d.ts +51 -0
  17. package/dist/commands/surveys.js +41 -0
  18. package/dist/commands/webhooks.d.ts +16 -0
  19. package/dist/commands/webhooks.js +28 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +29 -0
  22. package/dist/mcp.d.ts +2 -0
  23. package/dist/mcp.js +40 -0
  24. package/dist/tools/auth.d.ts +3 -0
  25. package/dist/tools/auth.js +75 -0
  26. package/dist/tools/content.d.ts +3 -0
  27. package/dist/tools/content.js +64 -0
  28. package/dist/tools/email.d.ts +3 -0
  29. package/dist/tools/email.js +57 -0
  30. package/dist/tools/journeys.d.ts +3 -0
  31. package/dist/tools/journeys.js +302 -0
  32. package/dist/tools/platform.d.ts +3 -0
  33. package/dist/tools/platform.js +63 -0
  34. package/dist/tools/surveys.d.ts +3 -0
  35. package/dist/tools/surveys.js +212 -0
  36. package/dist/tools/webhooks.d.ts +3 -0
  37. package/dist/tools/webhooks.js +56 -0
  38. package/dist/types.d.ts +96 -0
  39. package/dist/types.js +4 -0
  40. package/dist/utils/credentials.d.ts +27 -0
  41. package/dist/utils/credentials.js +90 -0
  42. package/dist/utils/duration.d.ts +16 -0
  43. package/dist/utils/duration.js +47 -0
  44. package/dist/utils/journey-format.d.ts +112 -0
  45. package/dist/utils/journey-format.js +200 -0
  46. package/dist/utils/survey-format.d.ts +60 -0
  47. package/dist/utils/survey-format.js +164 -0
  48. package/package.json +37 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1153 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from "fs";
3
+ import { AscendKitClient } from "./api/client.js";
4
+ import { loadAuth, loadEnvContext } from "./utils/credentials.js";
5
+ import * as auth from "./commands/auth.js";
6
+ import * as content from "./commands/content.js";
7
+ import * as surveys from "./commands/surveys.js";
8
+ import * as platform from "./commands/platform.js";
9
+ import * as journeys from "./commands/journeys.js";
10
+ import * as email from "./commands/email.js";
11
+ import { parseDelay } from "./utils/duration.js";
12
+ const HELP = `ascendkit - AscendKit CLI
13
+
14
+ Usage: ascendkit <command> [options]
15
+ ascendkit help [section]
16
+
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
26
+
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
43
+
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
64
+
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
+ `;
159
+ const HELP_SECTION = {
160
+ auth: `Usage: ascendkit auth <command>
161
+
162
+ Commands:
163
+ auth settings
164
+ auth settings update --providers <p1,p2,...> [--email-verification <true|false>] [--waitlist <true|false>] [--password-reset <true|false>] [--session-duration <duration>]
165
+ auth providers <p1,p2,...>
166
+ auth oauth <provider>
167
+ auth oauth set <provider> --client-id <id> [--client-secret <secret> | --client-secret-stdin] [--callback-url <url>]
168
+ auth users`,
169
+ content: `Usage: ascendkit content <command>
170
+
171
+ 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>`,
179
+ survey: `Usage: ascendkit survey <command>
180
+
181
+ Commands:
182
+ survey create --name <name> [--type <nps|csat|custom>] [--definition <json>]
183
+ survey list
184
+ survey get <survey-id>
185
+ survey update <survey-id> [--name <name>] [--status <draft|active|paused>] [--definition <json>]
186
+ survey delete <survey-id>
187
+ survey distribute <survey-id> --users <usr_id1,usr_id2,...>
188
+ survey invitations <survey-id>
189
+ survey analytics <survey-id>
190
+ survey export-definition <survey-id> [--out <file>]
191
+ survey import-definition <survey-id> --in <file>
192
+
193
+ Notes:
194
+ - Question-level editing commands are available via MCP tools, not this terminal CLI.
195
+ - import-definition only updates the survey definition. It does not mutate slug/name/status.`,
196
+ journey: `Usage: ascendkit journey <command>
197
+
198
+ Commands:
199
+ journey create --name <name> --entry-event <event> --entry-node <node> [--nodes <json>] [--transitions <json>] [--description <description>] [--entry-conditions <json>] [--re-entry-policy <skip|restart>]
200
+ journey list [--status <draft|active|paused|archived>]
201
+ journey get <journey-id>
202
+ 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>]
203
+ journey delete <journey-id>
204
+ journey activate <journey-id>
205
+ journey pause <journey-id>
206
+ journey archive <journey-id>
207
+ journey analytics <journey-id>
208
+ journey list-nodes <journey-id>
209
+ journey add-node <journey-id> --name <node-name> [--action <json>] [--terminal <true|false>]
210
+ journey edit-node <journey-id> <node-name> [--action <json>] [--terminal <true|false>]
211
+ journey remove-node <journey-id> <node-name>
212
+ journey list-transitions <journey-id> [--from <node-name>] [--to <node-name>]
213
+ journey add-transition <journey-id> --from <node-name> --to <node-name> --trigger <json> [--priority <n>] [--name <transition-name>]
214
+ journey edit-transition <journey-id> <transition-name> [--trigger <json>] [--priority <n>]
215
+ journey remove-transition <journey-id> <transition-name>`,
216
+ email: `Usage: ascendkit email <command>
217
+
218
+ Commands:
219
+ email settings
220
+ email settings update [--from-email <email>] [--from-name <name>]
221
+ email identity
222
+ email use-default
223
+ email use-custom <domain> [--from-email <email>] [--from-name <name>]
224
+ email setup-domain <domain>
225
+ email domain-status [--watch] [--interval <seconds>]
226
+ email open-dns [--domain <domain>] [--open]
227
+ email remove-domain`,
228
+ env: `Usage: ascendkit env <command>
229
+
230
+ Commands:
231
+ env list --project <project-id>
232
+ env use <tier> --project <project-id>`,
233
+ projects: `Usage: ascendkit projects list`,
234
+ };
235
+ function printSectionHelp(section) {
236
+ if (!section)
237
+ return false;
238
+ const key = section.toLowerCase();
239
+ const text = HELP_SECTION[key];
240
+ if (!text)
241
+ return false;
242
+ console.log(text);
243
+ return true;
244
+ }
245
+ function getClient() {
246
+ let publicKey = process.env.ASCENDKIT_PUBLIC_KEY;
247
+ let apiUrl = process.env.ASCENDKIT_API_URL;
248
+ const auth = loadAuth();
249
+ const env = loadEnvContext();
250
+ // Require auth unless env var provides the public key (CI/CD escape hatch)
251
+ if (!auth?.token && !publicKey) {
252
+ console.error("Not initialized. Run: ascendkit init");
253
+ process.exit(1);
254
+ }
255
+ if (!publicKey && env?.publicKey) {
256
+ publicKey = env.publicKey;
257
+ }
258
+ if (!apiUrl && auth?.apiUrl) {
259
+ apiUrl = auth.apiUrl;
260
+ }
261
+ if (!publicKey) {
262
+ console.error("No environment set. Run: ascendkit set-env <public-key>");
263
+ process.exit(1);
264
+ }
265
+ const client = new AscendKitClient({
266
+ apiUrl: apiUrl ?? "https://api.ascendkit.com",
267
+ publicKey,
268
+ });
269
+ if (auth?.token) {
270
+ client.configurePlatform(auth.token);
271
+ }
272
+ return client;
273
+ }
274
+ function parseFlags(args) {
275
+ const flags = {};
276
+ for (let i = 0; i < args.length; i++) {
277
+ if (args[i].startsWith("--")) {
278
+ const key = args[i].slice(2);
279
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
280
+ flags[key] = args[++i];
281
+ }
282
+ else {
283
+ flags[key] = "true";
284
+ }
285
+ }
286
+ }
287
+ return flags;
288
+ }
289
+ async function readSecretFromStdin() {
290
+ const chunks = [];
291
+ for await (const chunk of process.stdin) {
292
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
293
+ }
294
+ return Buffer.concat(chunks).toString("utf8").trim();
295
+ }
296
+ function output(data) {
297
+ console.log(JSON.stringify(data, null, 2));
298
+ }
299
+ function normalizeJourneyRows(data) {
300
+ if (Array.isArray(data)) {
301
+ return data;
302
+ }
303
+ if (data && typeof data === "object") {
304
+ const rows = data.journeys;
305
+ if (Array.isArray(rows)) {
306
+ return rows;
307
+ }
308
+ }
309
+ return [];
310
+ }
311
+ function table(rows, columns) {
312
+ if (rows.length === 0) {
313
+ console.log("No results.");
314
+ return;
315
+ }
316
+ // Compute column widths
317
+ const widths = columns.map(col => {
318
+ const max = Math.max(col.label.length, ...rows.map(r => String(r[col.key] ?? "").length));
319
+ return col.width ? Math.min(max, col.width) : max;
320
+ });
321
+ // Header
322
+ const header = columns.map((col, i) => col.label.padEnd(widths[i])).join(" ");
323
+ console.log(header);
324
+ console.log(columns.map((_, i) => "─".repeat(widths[i])).join(" "));
325
+ // Rows
326
+ for (const row of rows) {
327
+ const line = columns.map((col, i) => {
328
+ const val = String(row[col.key] ?? "");
329
+ return val.length > widths[i] ? val.slice(0, widths[i] - 1) + "…" : val.padEnd(widths[i]);
330
+ }).join(" ");
331
+ console.log(line);
332
+ }
333
+ }
334
+ async function run() {
335
+ const args = process.argv.slice(2);
336
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
337
+ console.log(HELP);
338
+ return;
339
+ }
340
+ if (args[0] === "help") {
341
+ if (args[1]) {
342
+ if (!printSectionHelp(args[1])) {
343
+ console.error(`Unknown help section: ${args[1]}`);
344
+ console.error('Run "ascendkit --help" for available sections.');
345
+ process.exit(1);
346
+ }
347
+ return;
348
+ }
349
+ console.log(HELP);
350
+ return;
351
+ }
352
+ const domain = args[0];
353
+ const action = args[1];
354
+ if (!action && printSectionHelp(domain)) {
355
+ return;
356
+ }
357
+ if (action === "--help" || action === "-h" || action === "help") {
358
+ if (!printSectionHelp(domain)) {
359
+ console.error(`Unknown command section: ${domain}`);
360
+ console.error('Run "ascendkit --help" for usage');
361
+ process.exit(1);
362
+ }
363
+ return;
364
+ }
365
+ // Platform commands (don't need environment key)
366
+ switch (domain) {
367
+ case "init": {
368
+ const flags = parseFlags(args.slice(1));
369
+ await platform.init(flags["backend"], flags["portal"]);
370
+ return;
371
+ }
372
+ case "logout":
373
+ platform.logout();
374
+ return;
375
+ case "projects":
376
+ if (action === "list") {
377
+ const projects = await platform.listProjects();
378
+ table(projects, [
379
+ { key: "id", label: "ID" },
380
+ { key: "name", label: "Name", width: 30 },
381
+ { key: "enabledServices", label: "Services", width: 30 },
382
+ ]);
383
+ }
384
+ else {
385
+ console.error('Usage: ascendkit projects list');
386
+ process.exit(1);
387
+ }
388
+ return;
389
+ case "set-env":
390
+ if (!action) {
391
+ console.error("Usage: ascendkit set-env <public-key>");
392
+ process.exit(1);
393
+ }
394
+ await platform.setEnv(action);
395
+ return;
396
+ case "status":
397
+ runStatus();
398
+ return;
399
+ case "verify":
400
+ await runVerify();
401
+ return;
402
+ case "env":
403
+ await runEnv(action, args.slice(2));
404
+ return;
405
+ }
406
+ // Service commands (need environment key)
407
+ const client = getClient();
408
+ switch (domain) {
409
+ case "auth":
410
+ await runAuth(client, action, args.slice(2));
411
+ break;
412
+ case "content":
413
+ await runContent(client, action, args.slice(2));
414
+ break;
415
+ case "survey":
416
+ await runSurvey(client, action, args.slice(2));
417
+ break;
418
+ case "journey":
419
+ await runJourney(client, action, args.slice(2));
420
+ break;
421
+ case "email":
422
+ await runEmail(client, action, args.slice(2));
423
+ break;
424
+ default:
425
+ console.error(`Unknown command: ${domain}`);
426
+ console.error('Run "ascendkit --help" for usage');
427
+ process.exit(1);
428
+ }
429
+ }
430
+ async function runEnv(action, rest) {
431
+ const flags = parseFlags(rest);
432
+ switch (action) {
433
+ case "list":
434
+ if (!flags.project) {
435
+ console.error("Usage: ascendkit env list --project <project-id>");
436
+ process.exit(1);
437
+ }
438
+ table(await platform.listEnvironments(flags.project), [
439
+ { key: "name", label: "Name", width: 20 },
440
+ { key: "tier", label: "Tier" },
441
+ { key: "publicKey", label: "Public Key" },
442
+ ]);
443
+ break;
444
+ case "use":
445
+ if (!rest[0] || !flags.project) {
446
+ console.error("Usage: ascendkit env use <tier> --project <project-id>");
447
+ process.exit(1);
448
+ }
449
+ await platform.useEnvironment(rest[0], flags.project);
450
+ break;
451
+ default:
452
+ console.error(`Unknown env command: ${action}`);
453
+ console.error("Usage: ascendkit env list|use");
454
+ process.exit(1);
455
+ }
456
+ }
457
+ async function runAuth(client, action, rest) {
458
+ const flags = parseFlags(rest);
459
+ switch (action) {
460
+ case "settings":
461
+ if (rest[0] === "update") {
462
+ const params = {};
463
+ if (flags.providers)
464
+ params.providers = flags.providers.split(",");
465
+ if (flags["email-verification"] || flags.waitlist || flags["password-reset"]) {
466
+ params.features = {};
467
+ if (flags["email-verification"])
468
+ params.features.emailVerification = flags["email-verification"] === "true";
469
+ if (flags.waitlist)
470
+ params.features.waitlist = flags.waitlist === "true";
471
+ if (flags["password-reset"])
472
+ params.features.passwordReset = flags["password-reset"] === "true";
473
+ }
474
+ if (flags["session-duration"])
475
+ params.sessionDuration = flags["session-duration"];
476
+ output(await auth.updateSettings(client, params));
477
+ }
478
+ else {
479
+ output(await auth.getSettings(client));
480
+ }
481
+ break;
482
+ case "providers":
483
+ if (!rest[0]) {
484
+ console.error("Usage: ascendkit auth providers <p1,p2,...>");
485
+ process.exit(1);
486
+ }
487
+ output(await auth.updateProviders(client, rest[0].split(",")));
488
+ break;
489
+ case "oauth": {
490
+ if (rest[0] === "set") {
491
+ const provider = rest[1];
492
+ if (!provider || !flags["client-id"]) {
493
+ console.error("Usage: ascendkit auth oauth set <provider> --client-id <id> [--client-secret <secret> | --client-secret-stdin] [--callback-url <url>]");
494
+ process.exit(1);
495
+ }
496
+ const secretFromArg = flags["client-secret"];
497
+ const secretFromStdin = flags["client-secret-stdin"] === "true";
498
+ if (!secretFromArg && !secretFromStdin) {
499
+ console.error("Missing client secret. Use --client-secret-stdin (recommended) or --client-secret.");
500
+ process.exit(1);
501
+ }
502
+ if (secretFromArg && secretFromStdin) {
503
+ console.error("Use only one of --client-secret or --client-secret-stdin.");
504
+ process.exit(1);
505
+ }
506
+ const clientSecret = secretFromArg ?? await readSecretFromStdin();
507
+ if (!clientSecret) {
508
+ console.error("Client secret cannot be empty.");
509
+ process.exit(1);
510
+ }
511
+ output(await auth.updateOAuthCredentials(client, provider, flags["client-id"], clientSecret, flags["callback-url"]));
512
+ }
513
+ else {
514
+ if (!rest[0]) {
515
+ console.error("Usage: ascendkit auth oauth <provider>");
516
+ process.exit(1);
517
+ }
518
+ const portalUrl = process.env.ASCENDKIT_PORTAL_URL ?? "http://localhost:3000";
519
+ const url = auth.getOAuthSetupUrl(portalUrl, rest[0], client.currentPublicKey ?? undefined);
520
+ console.log(`Opening browser to configure ${rest[0]} OAuth credentials...`);
521
+ console.log(url);
522
+ openBrowser(url);
523
+ }
524
+ break;
525
+ }
526
+ case "users":
527
+ table(await auth.listUsers(client), [
528
+ { key: "id", label: "ID" },
529
+ { key: "email", label: "Email", width: 35 },
530
+ { key: "name", label: "Name", width: 25 },
531
+ { key: "status", label: "Status" },
532
+ ]);
533
+ break;
534
+ default:
535
+ console.error(`Unknown auth command: ${action}`);
536
+ process.exit(1);
537
+ }
538
+ }
539
+ async function runContent(client, action, rest) {
540
+ const flags = parseFlags(rest);
541
+ switch (action) {
542
+ case "create":
543
+ 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>]");
545
+ process.exit(1);
546
+ }
547
+ output(await content.createTemplate(client, {
548
+ name: flags.name, subject: flags.subject,
549
+ bodyHtml: flags["body-html"], bodyText: flags["body-text"],
550
+ slug: flags.slug, description: flags.description,
551
+ }));
552
+ break;
553
+ case "list": {
554
+ const listParams = {};
555
+ if (flags.query)
556
+ listParams.query = flags.query;
557
+ if (flags.system === "true")
558
+ listParams.isSystem = true;
559
+ else if (flags.custom === "true")
560
+ listParams.isSystem = false;
561
+ const templates = await content.listTemplates(client, Object.keys(listParams).length > 0 ? listParams : undefined);
562
+ table(templates, [
563
+ { key: "id", label: "ID" },
564
+ { key: "name", label: "Name", width: 30 },
565
+ { key: "slug", label: "Slug", width: 25 },
566
+ { key: "subject", label: "Subject", width: 30 },
567
+ ]);
568
+ break;
569
+ }
570
+ case "get":
571
+ if (!rest[0]) {
572
+ console.error("Usage: ascendkit content get <template-id>");
573
+ process.exit(1);
574
+ }
575
+ output(await content.getTemplate(client, rest[0]));
576
+ break;
577
+ case "update":
578
+ if (!rest[0]) {
579
+ console.error("Usage: ascendkit content update <template-id> [--flags]");
580
+ process.exit(1);
581
+ }
582
+ output(await content.updateTemplate(client, rest[0], {
583
+ subject: flags.subject,
584
+ bodyHtml: flags["body-html"],
585
+ bodyText: flags["body-text"],
586
+ changeNote: flags["change-note"],
587
+ }));
588
+ break;
589
+ case "delete":
590
+ if (!rest[0]) {
591
+ console.error("Usage: ascendkit content delete <template-id>");
592
+ process.exit(1);
593
+ }
594
+ output(await content.deleteTemplate(client, rest[0]));
595
+ break;
596
+ case "versions":
597
+ if (!rest[0]) {
598
+ console.error("Usage: ascendkit content versions <template-id>");
599
+ process.exit(1);
600
+ }
601
+ output(await content.listVersions(client, rest[0]));
602
+ break;
603
+ case "version":
604
+ if (!rest[0] || !rest[1]) {
605
+ console.error("Usage: ascendkit content version <template-id> <n>");
606
+ process.exit(1);
607
+ }
608
+ output(await content.getVersion(client, rest[0], parseInt(rest[1], 10)));
609
+ break;
610
+ default:
611
+ console.error(`Unknown content command: ${action}`);
612
+ process.exit(1);
613
+ }
614
+ }
615
+ async function runSurvey(client, action, rest) {
616
+ const flags = parseFlags(rest);
617
+ if (!action) {
618
+ console.log(HELP_SECTION.survey);
619
+ return;
620
+ }
621
+ switch (action) {
622
+ case "create":
623
+ if (!flags.name) {
624
+ console.error("Usage: ascendkit survey create --name <n> [--type nps|csat|custom]");
625
+ process.exit(1);
626
+ }
627
+ output(await surveys.createSurvey(client, {
628
+ name: flags.name,
629
+ type: flags.type ?? "custom",
630
+ definition: flags.definition ? JSON.parse(flags.definition) : undefined,
631
+ }));
632
+ break;
633
+ case "list":
634
+ table(await surveys.listSurveys(client), [
635
+ { key: "id", label: "ID" },
636
+ { key: "name", label: "Name", width: 30 },
637
+ { key: "type", label: "Type" },
638
+ { key: "status", label: "Status" },
639
+ ]);
640
+ break;
641
+ case "get":
642
+ if (!rest[0]) {
643
+ console.error("Usage: ascendkit survey get <survey-id>");
644
+ process.exit(1);
645
+ }
646
+ output(await surveys.getSurvey(client, rest[0]));
647
+ break;
648
+ case "update":
649
+ if (!rest[0]) {
650
+ console.error("Usage: ascendkit survey update <survey-id> [--flags]");
651
+ process.exit(1);
652
+ }
653
+ output(await surveys.updateSurvey(client, rest[0], {
654
+ name: flags.name,
655
+ status: flags.status,
656
+ definition: flags.definition ? JSON.parse(flags.definition) : undefined,
657
+ }));
658
+ break;
659
+ case "delete":
660
+ if (!rest[0]) {
661
+ console.error("Usage: ascendkit survey delete <survey-id>");
662
+ process.exit(1);
663
+ }
664
+ output(await surveys.deleteSurvey(client, rest[0]));
665
+ break;
666
+ case "distribute":
667
+ if (!rest[0] || !flags.users) {
668
+ console.error("Usage: ascendkit survey distribute <survey-id> --users <usr_id1,usr_id2,...>");
669
+ process.exit(1);
670
+ }
671
+ output(await surveys.distributeSurvey(client, rest[0], flags.users.split(",")));
672
+ break;
673
+ case "invitations":
674
+ if (!rest[0]) {
675
+ console.error("Usage: ascendkit survey invitations <survey-id>");
676
+ process.exit(1);
677
+ }
678
+ output(await surveys.listInvitations(client, rest[0]));
679
+ break;
680
+ case "analytics":
681
+ if (!rest[0]) {
682
+ console.error("Usage: ascendkit survey analytics <survey-id>");
683
+ process.exit(1);
684
+ }
685
+ output(await surveys.getAnalytics(client, rest[0]));
686
+ break;
687
+ case "export-definition": {
688
+ if (!rest[0]) {
689
+ console.error("Usage: ascendkit survey export-definition <survey-id> [--out <file>]");
690
+ process.exit(1);
691
+ }
692
+ const survey = await surveys.getSurvey(client, rest[0]);
693
+ const text = `${JSON.stringify(survey.definition ?? {}, null, 2)}\n`;
694
+ const outPath = flags.out || flags.file;
695
+ if (outPath) {
696
+ writeFileSync(outPath, text, "utf8");
697
+ console.log(`Wrote survey definition to ${outPath}`);
698
+ }
699
+ else {
700
+ process.stdout.write(text);
701
+ }
702
+ break;
703
+ }
704
+ case "import-definition": {
705
+ if (!rest[0] || !(flags.in || flags.file)) {
706
+ console.error("Usage: ascendkit survey import-definition <survey-id> --in <file>");
707
+ process.exit(1);
708
+ }
709
+ const inPath = flags.in || flags.file;
710
+ const raw = readFileSync(inPath, "utf8");
711
+ const parsed = JSON.parse(raw);
712
+ const candidate = parsed && typeof parsed === "object" && "definition" in parsed
713
+ ? parsed.definition
714
+ : parsed;
715
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
716
+ console.error("Invalid definition JSON: expected an object or { definition: object }");
717
+ process.exit(1);
718
+ }
719
+ output(await surveys.updateSurvey(client, rest[0], {
720
+ definition: candidate,
721
+ }));
722
+ break;
723
+ }
724
+ default:
725
+ console.error(`Unknown survey command: ${action}`);
726
+ console.error('Run "ascendkit survey --help" for usage');
727
+ process.exit(1);
728
+ }
729
+ }
730
+ function runStatus() {
731
+ const authData = loadAuth();
732
+ const env = loadEnvContext();
733
+ console.log("\nLogin:");
734
+ if (authData?.token) {
735
+ console.log(` Logged in (API: ${authData.apiUrl})`);
736
+ }
737
+ else {
738
+ console.log(" Not initialized. Run: ascendkit init");
739
+ }
740
+ console.log("\nEnvironment:");
741
+ if (env?.publicKey) {
742
+ console.log(` Project: ${env.projectName || env.projectId}`);
743
+ console.log(` Environment: ${env.environmentName || env.environmentId} (${env.tier})`);
744
+ console.log(` Public key: ${env.publicKey}`);
745
+ }
746
+ else {
747
+ console.log(" No environment set. Run: ascendkit set-env <public-key>");
748
+ console.log(" List environments: ascendkit env list --project <project-id>");
749
+ }
750
+ console.log();
751
+ }
752
+ async function runVerify() {
753
+ const client = getClient();
754
+ const checks = [
755
+ {
756
+ service: "Auth",
757
+ check: async () => {
758
+ const s = await auth.getSettings(client);
759
+ const providers = s?.providers ?? [];
760
+ return providers.length > 0 ? `configured (${providers.join(", ")})` : "no providers configured";
761
+ },
762
+ },
763
+ {
764
+ service: "Email",
765
+ check: async () => {
766
+ const s = await email.getSettings(client);
767
+ if (s?.domain)
768
+ return `domain ${s.verificationStatus === "verified" ? "verified" : "pending DNS verification"} (${s.domain})`;
769
+ return "no domain configured (using AscendKit default sender)";
770
+ },
771
+ },
772
+ {
773
+ service: "Content",
774
+ check: async () => {
775
+ const items = await content.listTemplates(client);
776
+ return `${items.length} template${items.length !== 1 ? "s" : ""}`;
777
+ },
778
+ },
779
+ {
780
+ service: "Surveys",
781
+ check: async () => {
782
+ const items = await surveys.listSurveys(client);
783
+ const active = items.filter(s => s.status === "active").length;
784
+ return `${items.length} survey${items.length !== 1 ? "s" : ""}${active > 0 ? ` (${active} active)` : ""}`;
785
+ },
786
+ },
787
+ {
788
+ service: "Journeys",
789
+ check: async () => {
790
+ const raw = await journeys.listJourneys(client);
791
+ const items = normalizeJourneyRows(raw);
792
+ const active = items.filter(j => j.status === "active").length;
793
+ return `${items.length} journey${items.length !== 1 ? "s" : ""}${active > 0 ? ` (${active} active)` : ""}`;
794
+ },
795
+ },
796
+ ];
797
+ console.log();
798
+ const results = await Promise.allSettled(checks.map(c => c.check()));
799
+ for (let i = 0; i < checks.length; i++) {
800
+ const r = results[i];
801
+ const label = checks[i].service.padEnd(12);
802
+ if (r.status === "fulfilled") {
803
+ console.log(` ${label} ✓ ${r.value}`);
804
+ }
805
+ else {
806
+ console.log(` ${label} ✗ ${r.reason?.message ?? "unavailable"}`);
807
+ }
808
+ }
809
+ console.log();
810
+ }
811
+ async function runJourney(client, action, rest) {
812
+ const flags = parseFlags(rest);
813
+ switch (action) {
814
+ case "create":
815
+ if (!flags.name || !flags["entry-event"] || !flags["entry-node"]) {
816
+ 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>]");
817
+ process.exit(1);
818
+ }
819
+ output(await journeys.createJourney(client, {
820
+ name: flags.name,
821
+ entryEvent: flags["entry-event"],
822
+ entryNode: flags["entry-node"],
823
+ nodes: flags.nodes ? JSON.parse(flags.nodes) : undefined,
824
+ transitions: flags.transitions ? JSON.parse(flags.transitions) : undefined,
825
+ description: flags.description,
826
+ entryConditions: flags["entry-conditions"] ? JSON.parse(flags["entry-conditions"]) : undefined,
827
+ reEntryPolicy: flags["re-entry-policy"],
828
+ }));
829
+ break;
830
+ case "list": {
831
+ const opts = {};
832
+ if (flags.status)
833
+ opts.status = flags.status;
834
+ const raw = await journeys.listJourneys(client, Object.keys(opts).length > 0 ? opts : undefined);
835
+ const items = normalizeJourneyRows(raw);
836
+ table(items, [
837
+ { key: "id", label: "ID" },
838
+ { key: "name", label: "Name", width: 30 },
839
+ { key: "status", label: "Status" },
840
+ { key: "entryEvent", label: "Entry Event", width: 20 },
841
+ ]);
842
+ break;
843
+ }
844
+ case "get":
845
+ if (!rest[0]) {
846
+ console.error("Usage: ascendkit journey get <journey-id>");
847
+ process.exit(1);
848
+ }
849
+ output(await journeys.getJourney(client, rest[0]));
850
+ break;
851
+ case "update":
852
+ if (!rest[0]) {
853
+ console.error("Usage: ascendkit journey update <journey-id> [--flags]");
854
+ process.exit(1);
855
+ }
856
+ output(await journeys.updateJourney(client, rest[0], {
857
+ name: flags.name,
858
+ description: flags.description,
859
+ entryEvent: flags["entry-event"],
860
+ entryNode: flags["entry-node"],
861
+ entryConditions: flags["entry-conditions"] ? JSON.parse(flags["entry-conditions"]) : undefined,
862
+ reEntryPolicy: flags["re-entry-policy"],
863
+ nodes: flags.nodes ? JSON.parse(flags.nodes) : undefined,
864
+ transitions: flags.transitions ? JSON.parse(flags.transitions) : undefined,
865
+ }));
866
+ break;
867
+ case "delete":
868
+ if (!rest[0]) {
869
+ console.error("Usage: ascendkit journey delete <journey-id>");
870
+ process.exit(1);
871
+ }
872
+ output(await journeys.deleteJourney(client, rest[0]));
873
+ break;
874
+ case "activate":
875
+ if (!rest[0]) {
876
+ console.error("Usage: ascendkit journey activate <journey-id>");
877
+ process.exit(1);
878
+ }
879
+ output(await journeys.activateJourney(client, rest[0]));
880
+ break;
881
+ case "pause":
882
+ if (!rest[0]) {
883
+ console.error("Usage: ascendkit journey pause <journey-id>");
884
+ process.exit(1);
885
+ }
886
+ output(await journeys.pauseJourney(client, rest[0]));
887
+ break;
888
+ case "archive":
889
+ if (!rest[0]) {
890
+ console.error("Usage: ascendkit journey archive <journey-id>");
891
+ process.exit(1);
892
+ }
893
+ output(await journeys.archiveJourney(client, rest[0]));
894
+ break;
895
+ case "analytics":
896
+ if (!rest[0]) {
897
+ console.error("Usage: ascendkit journey analytics <journey-id>");
898
+ process.exit(1);
899
+ }
900
+ output(await journeys.getJourneyAnalytics(client, rest[0]));
901
+ break;
902
+ case "list-nodes":
903
+ if (!rest[0]) {
904
+ console.error("Usage: ascendkit journey list-nodes <journey-id>");
905
+ process.exit(1);
906
+ }
907
+ output(await journeys.listNodes(client, rest[0]));
908
+ break;
909
+ case "add-node": {
910
+ if (!rest[0] || !flags.name) {
911
+ console.error("Usage: ascendkit journey add-node <journey-id> --name <node-name> [--action <json>] [--terminal <true|false>]");
912
+ process.exit(1);
913
+ }
914
+ const params = { name: flags.name };
915
+ if (flags.action)
916
+ params.action = JSON.parse(flags.action);
917
+ if (flags.terminal)
918
+ params.terminal = flags.terminal === "true";
919
+ output(await journeys.addNode(client, rest[0], params));
920
+ break;
921
+ }
922
+ case "edit-node": {
923
+ if (!rest[0] || !rest[1]) {
924
+ console.error("Usage: ascendkit journey edit-node <journey-id> <node-name> [--action <json>] [--terminal <true|false>]");
925
+ process.exit(1);
926
+ }
927
+ const params = {};
928
+ if (flags.action)
929
+ params.action = JSON.parse(flags.action);
930
+ if (flags.terminal)
931
+ params.terminal = flags.terminal === "true";
932
+ output(await journeys.editNode(client, rest[0], rest[1], params));
933
+ break;
934
+ }
935
+ case "remove-node":
936
+ if (!rest[0] || !rest[1]) {
937
+ console.error("Usage: ascendkit journey remove-node <journey-id> <node-name>");
938
+ process.exit(1);
939
+ }
940
+ output(await journeys.removeNode(client, rest[0], rest[1]));
941
+ break;
942
+ case "list-transitions": {
943
+ if (!rest[0]) {
944
+ console.error("Usage: ascendkit journey list-transitions <journey-id> [--from <node-name>] [--to <node-name>]");
945
+ process.exit(1);
946
+ }
947
+ output(await journeys.listTransitions(client, rest[0], {
948
+ from_node: flags.from,
949
+ to_node: flags.to,
950
+ }));
951
+ break;
952
+ }
953
+ case "add-transition": {
954
+ if (!rest[0] || !flags.from || !flags.to || !(flags.on || flags.after || flags.trigger)) {
955
+ 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>]");
956
+ process.exit(1);
957
+ }
958
+ let trigger;
959
+ if (flags.on) {
960
+ trigger = { type: "event", event: flags.on };
961
+ }
962
+ else if (flags.after) {
963
+ trigger = { type: "timer", delay: parseDelay(flags.after) };
964
+ }
965
+ else {
966
+ trigger = JSON.parse(flags.trigger);
967
+ }
968
+ const params = {
969
+ from: flags.from,
970
+ to: flags.to,
971
+ trigger,
972
+ };
973
+ if (flags.priority != null)
974
+ params.priority = Number(flags.priority);
975
+ if (flags.name)
976
+ params.name = flags.name;
977
+ output(await journeys.addTransition(client, rest[0], params));
978
+ break;
979
+ }
980
+ case "edit-transition": {
981
+ if (!rest[0] || !rest[1]) {
982
+ console.error("Usage: ascendkit journey edit-transition <journey-id> <transition-name> [--on <event>] [--after <delay>] [--trigger <json>] [--priority <n>]");
983
+ process.exit(1);
984
+ }
985
+ const params = {};
986
+ if (flags.on) {
987
+ params.trigger = { type: "event", event: flags.on };
988
+ }
989
+ else if (flags.after) {
990
+ params.trigger = { type: "timer", delay: parseDelay(flags.after) };
991
+ }
992
+ else if (flags.trigger) {
993
+ params.trigger = JSON.parse(flags.trigger);
994
+ }
995
+ if (flags.priority != null)
996
+ params.priority = Number(flags.priority);
997
+ output(await journeys.editTransition(client, rest[0], rest[1], params));
998
+ break;
999
+ }
1000
+ case "remove-transition":
1001
+ if (!rest[0] || !rest[1]) {
1002
+ console.error("Usage: ascendkit journey remove-transition <journey-id> <transition-name>");
1003
+ process.exit(1);
1004
+ }
1005
+ output(await journeys.removeTransition(client, rest[0], rest[1]));
1006
+ break;
1007
+ default:
1008
+ console.error(`Unknown journey command: ${action}`);
1009
+ process.exit(1);
1010
+ }
1011
+ }
1012
+ async function runEmail(client, action, rest) {
1013
+ const flags = parseFlags(rest);
1014
+ switch (action) {
1015
+ case "identity": {
1016
+ const s = await email.getSettings(client);
1017
+ printIdentityStatus(s);
1018
+ break;
1019
+ }
1020
+ case "use-default": {
1021
+ const s = await email.useDefaultIdentity(client);
1022
+ printIdentityStatus(s);
1023
+ break;
1024
+ }
1025
+ case "use-custom": {
1026
+ if (!rest[0]) {
1027
+ console.error("Usage: ascendkit email use-custom <domain> [--from-email <email>] [--from-name <name>]");
1028
+ process.exit(1);
1029
+ }
1030
+ const s = await email.useCustomIdentity(client, rest[0], {
1031
+ fromEmail: flags["from-email"],
1032
+ fromName: flags["from-name"],
1033
+ });
1034
+ await printEmailSetup(s);
1035
+ break;
1036
+ }
1037
+ case "settings":
1038
+ if (rest[0] === "update") {
1039
+ const params = {};
1040
+ if (flags["from-email"])
1041
+ params.fromEmail = flags["from-email"];
1042
+ if (flags["from-name"])
1043
+ params.fromName = flags["from-name"];
1044
+ output(await email.updateSettings(client, params));
1045
+ }
1046
+ else {
1047
+ output(await email.getSettings(client));
1048
+ }
1049
+ break;
1050
+ case "setup-domain":
1051
+ if (!rest[0]) {
1052
+ console.error("Usage: ascendkit email setup-domain <domain>");
1053
+ process.exit(1);
1054
+ }
1055
+ await printEmailSetup(await email.setupDomain(client, rest[0]));
1056
+ break;
1057
+ case "domain-status": {
1058
+ const interval = Math.max(5, Number.parseInt(flags.interval || "15", 10) || 15);
1059
+ if (flags.watch === "true") {
1060
+ await watchDomainStatus(client, interval);
1061
+ }
1062
+ else {
1063
+ output(await email.checkDomainStatus(client));
1064
+ }
1065
+ break;
1066
+ }
1067
+ case "open-dns": {
1068
+ const provider = await email.getDnsProvider(client, flags.domain);
1069
+ const url = provider?.portalUrl;
1070
+ if (!url) {
1071
+ console.error("Could not determine DNS provider URL for this domain.");
1072
+ process.exit(1);
1073
+ }
1074
+ console.log(url);
1075
+ if (flags.open === "true") {
1076
+ openBrowser(url);
1077
+ }
1078
+ break;
1079
+ }
1080
+ case "remove-domain":
1081
+ output(await email.removeDomain(client));
1082
+ break;
1083
+ default:
1084
+ console.error(`Unknown email command: ${action}`);
1085
+ process.exit(1);
1086
+ }
1087
+ }
1088
+ run().catch((err) => {
1089
+ console.error(err.message);
1090
+ process.exit(1);
1091
+ });
1092
+ async function printEmailSetup(settings) {
1093
+ output(settings);
1094
+ if (!settings.domain)
1095
+ return;
1096
+ const provider = settings.dnsProvider;
1097
+ if (provider?.name) {
1098
+ console.log(`\nDetected DNS provider: ${provider.name} (${provider.confidence ?? "unknown"})`);
1099
+ }
1100
+ if (provider?.portalUrl) {
1101
+ console.log(`Provider console: ${provider.portalUrl}`);
1102
+ }
1103
+ if (provider?.assistantSetupUrl) {
1104
+ console.log(`Guided setup: ${provider.assistantSetupUrl}`);
1105
+ }
1106
+ if (Array.isArray(settings.dnsRecords) && settings.dnsRecords.length > 0) {
1107
+ console.log("\nAdd these DNS records:");
1108
+ for (const rec of settings.dnsRecords) {
1109
+ console.log(` ${rec.type}\t${rec.name}\t${rec.value}`);
1110
+ }
1111
+ }
1112
+ }
1113
+ function printIdentityStatus(settings) {
1114
+ const mode = settings.domain ? "customer-owned" : "ascendkit-default";
1115
+ const from = settings.fromEmail || "noreply@ascendkit.dev";
1116
+ const status = settings.verificationStatus || "none";
1117
+ console.log(`Identity mode: ${mode}`);
1118
+ console.log(`From email: ${from}`);
1119
+ console.log(`From name: ${settings.fromName || "not set"}`);
1120
+ if (settings.domain) {
1121
+ console.log(`Domain: ${settings.domain} (${status})`);
1122
+ if (status !== "verified") {
1123
+ console.log("Next step: add DNS records and run `ascendkit email domain-status --watch`");
1124
+ }
1125
+ }
1126
+ else {
1127
+ console.log("Next step: run `ascendkit email use-custom <domain>` to configure customer-owned identity.");
1128
+ }
1129
+ }
1130
+ async function watchDomainStatus(client, intervalSeconds) {
1131
+ while (true) {
1132
+ const data = await email.checkDomainStatus(client);
1133
+ output(data);
1134
+ if (data?.status === "verified" || data?.status === "failed" || data?.status === "none") {
1135
+ return;
1136
+ }
1137
+ await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));
1138
+ }
1139
+ }
1140
+ function openBrowser(url) {
1141
+ import("child_process").then(({ execFile }) => {
1142
+ if (process.platform === "darwin") {
1143
+ execFile("open", [url]);
1144
+ }
1145
+ else if (process.platform === "win32") {
1146
+ const safe = url.replace(/[&|<>^"]/g, "^$&");
1147
+ execFile("cmd.exe", ["/c", "start", "", safe]);
1148
+ }
1149
+ else {
1150
+ execFile("xdg-open", [url]);
1151
+ }
1152
+ });
1153
+ }