@zanfia/cli 0.3.52 → 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +655 -47
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8343,6 +8343,12 @@ function createMockClient() {
8343
8343
  async cancelEmailCampaign() {
8344
8344
  throw new ApiError(501, "email campaigns are not available in mock mode");
8345
8345
  },
8346
+ async sendEmailCampaignTest() {
8347
+ throw new ApiError(501, "email campaigns are not available in mock mode");
8348
+ },
8349
+ async resendEmailCampaign() {
8350
+ throw new ApiError(501, "email campaigns are not available in mock mode");
8351
+ },
8346
8352
  async listCampaignRecipients() {
8347
8353
  throw new ApiError(501, "email campaigns are not available in mock mode");
8348
8354
  },
@@ -8538,6 +8544,15 @@ function createMockClient() {
8538
8544
  async copyNewsletterIssues() {
8539
8545
  throw new ApiError(501, "newsletter content is not available in mock mode");
8540
8546
  },
8547
+ async sendNewsletterIssue() {
8548
+ throw new ApiError(501, "newsletter content is not available in mock mode");
8549
+ },
8550
+ async cancelNewsletterIssueSend() {
8551
+ throw new ApiError(501, "newsletter content is not available in mock mode");
8552
+ },
8553
+ async getNewsletterIssueSendReport() {
8554
+ throw new ApiError(501, "newsletter content is not available in mock mode");
8555
+ },
8541
8556
  async getKbContent() {
8542
8557
  throw new ApiError(501, "knowledge-base content is not available in mock mode");
8543
8558
  },
@@ -8577,6 +8592,21 @@ function createMockClient() {
8577
8592
  async getMediaTranscript() {
8578
8593
  throw new ApiError(501, "media transcripts are not available in mock mode");
8579
8594
  },
8595
+ async listMediaCaptions() {
8596
+ throw new ApiError(501, "video captions are not available in mock mode");
8597
+ },
8598
+ async uploadMediaCaption() {
8599
+ throw new ApiError(501, "video captions are not available in mock mode");
8600
+ },
8601
+ async getMediaCaption() {
8602
+ throw new ApiError(501, "video captions are not available in mock mode");
8603
+ },
8604
+ async deleteMediaCaption() {
8605
+ throw new ApiError(501, "video captions are not available in mock mode");
8606
+ },
8607
+ async regenerateMediaCaption() {
8608
+ throw new ApiError(501, "video captions are not available in mock mode");
8609
+ },
8580
8610
  async generateAIImage() {
8581
8611
  throw new ApiError(501, "AI image generation is not available in mock mode");
8582
8612
  },
@@ -8706,6 +8736,12 @@ function createMockClient() {
8706
8736
  async listSignupFormSubmissions() {
8707
8737
  return { submissions: [] };
8708
8738
  },
8739
+ async listLiveRooms() {
8740
+ return { rooms: [] };
8741
+ },
8742
+ async getLiveRoomAttendance() {
8743
+ throw new ApiError(501, "live rooms are not available in mock mode");
8744
+ },
8709
8745
  async call(method, pathTemplate) {
8710
8746
  throw new ApiError(501, `${method} ${pathTemplate} is not available in mock mode`);
8711
8747
  },
@@ -9502,6 +9538,12 @@ function createHttpClient(config) {
9502
9538
  scheduledAt !== void 0 ? { scheduledAt } : {}
9503
9539
  ),
9504
9540
  cancelEmailCampaign: (id) => request("POST", `/campaigns/${encodeURIComponent(id)}/cancel`),
9541
+ sendEmailCampaignTest: (id, recipients) => request(
9542
+ "POST",
9543
+ `/campaigns/${encodeURIComponent(id)}/test-sends`,
9544
+ recipients && recipients.length > 0 ? { recipients } : {}
9545
+ ),
9546
+ resendEmailCampaign: (id, target) => request("POST", `/campaigns/${encodeURIComponent(id)}/resend`, target ? { target } : {}),
9505
9547
  listCampaignRecipients: (id, options) => {
9506
9548
  const params = new URLSearchParams();
9507
9549
  if (options?.status) params.set("status", options.status);
@@ -9661,6 +9703,19 @@ function createHttpClient(config) {
9661
9703
  `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/draft`
9662
9704
  ),
9663
9705
  copyNewsletterIssues: (productId, input) => request("POST", `/products/${encodeURIComponent(productId)}/newsletter/issues/copy`, input),
9706
+ sendNewsletterIssue: (productId, issueId, input) => request(
9707
+ "POST",
9708
+ `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/send`,
9709
+ input
9710
+ ),
9711
+ cancelNewsletterIssueSend: (productId, issueId) => request(
9712
+ "POST",
9713
+ `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/send/cancel`
9714
+ ),
9715
+ getNewsletterIssueSendReport: (productId, issueId) => request(
9716
+ "GET",
9717
+ `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/send/report`
9718
+ ),
9664
9719
  // Knowledge-base product content (addressed by product id).
9665
9720
  getKbContent: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/kb`),
9666
9721
  getKbArticle: (productId, articleId) => request(
@@ -9701,6 +9756,17 @@ function createHttpClient(config) {
9701
9756
  "GET",
9702
9757
  `/media/${encodeURIComponent(mediaId)}/transcript${includeWords ? "?includeWords=true" : ""}`
9703
9758
  ),
9759
+ listMediaCaptions: (mediaId) => request("GET", `/media/${encodeURIComponent(mediaId)}/captions`),
9760
+ uploadMediaCaption: (mediaId, input) => request("POST", `/media/${encodeURIComponent(mediaId)}/captions`, input),
9761
+ getMediaCaption: (mediaId, srclang) => request(
9762
+ "GET",
9763
+ `/media/${encodeURIComponent(mediaId)}/captions/${encodeURIComponent(srclang)}`
9764
+ ),
9765
+ deleteMediaCaption: (mediaId, srclang) => request(
9766
+ "DELETE",
9767
+ `/media/${encodeURIComponent(mediaId)}/captions/${encodeURIComponent(srclang)}`
9768
+ ),
9769
+ regenerateMediaCaption: (mediaId) => request("POST", `/media/${encodeURIComponent(mediaId)}/captions/regenerate`, {}),
9704
9770
  // AI image generation + on-demand transcription (both spend credits / minutes).
9705
9771
  generateAIImage: (input) => request("POST", "/media/ai-images", input),
9706
9772
  transcribeMedia: (mediaId, input) => request("POST", `/media/${encodeURIComponent(mediaId)}/transcribe`, input),
@@ -9840,6 +9906,12 @@ function createHttpClient(config) {
9840
9906
  "GET",
9841
9907
  `/forms/${encodeURIComponent(id)}/submissions${limit !== void 0 ? `?limit=${limit}` : ""}`
9842
9908
  ),
9909
+ // Live rooms live on the Workspace API (api-v1), read only.
9910
+ listLiveRooms: () => request("GET", "/live-rooms"),
9911
+ getLiveRoomAttendance: (roomId, session) => request(
9912
+ "GET",
9913
+ `/live-rooms/${encodeURIComponent(roomId)}/attendance${session !== void 0 ? `?session=${encodeURIComponent(session)}` : ""}`
9914
+ ),
9843
9915
  // Descriptor-generated commands (certificates, …) — Workspace API (api-v1).
9844
9916
  call: (method, pathTemplate, params, query, body) => request(method, `${fillPathTemplate(pathTemplate, params)}${toQuery(query)}`, body),
9845
9917
  // Referral / affiliate programs live on the Workspace API (api-v1).
@@ -9946,6 +10018,14 @@ function parseNumberOption(flag, format) {
9946
10018
  return n;
9947
10019
  };
9948
10020
  }
10021
+ var WORKFLOW_KINDS = ["transactional", "marketing"];
10022
+ function parseWorkflowKind(value) {
10023
+ const kind = value.trim().toLowerCase();
10024
+ if (!WORKFLOW_KINDS.includes(kind)) {
10025
+ throw new Error(`Invalid --kind: ${value} (expected ${WORKFLOW_KINDS.join(" or ")})`);
10026
+ }
10027
+ return kind;
10028
+ }
9949
10029
  function parseCommaList(flag) {
9950
10030
  return (value) => {
9951
10031
  const items = [
@@ -10349,9 +10429,133 @@ function registerCertificatesCommands(program2) {
10349
10429
  });
10350
10430
  }
10351
10431
 
10432
+ // src/commands/_generated/blog.ts
10433
+ var listBlogPostsColumns = ["id", "slug", "title", "status", "publishedAt", "updatedAt"];
10434
+ function registerBlogCommands(program2) {
10435
+ const blog = program2.command("blog");
10436
+ blog.description(
10437
+ "The workspace blog (Your site \u2192 Blog): Markdown posts served at /blog on the store host; drafts are visible to the team only"
10438
+ );
10439
+ blog.command("list").summary("List blog posts").description(
10440
+ "Every post of the workspace blog (drafts and published), newest change first: id, slug (`/blog/{slug}` on the store host), title, excerpt, cover, tags, status and dates. No body - use get-blog-post for the Markdown. The id source for every other blog tool."
10441
+ ).action(async (_options, command) => {
10442
+ const flags = getGlobalFlags(command);
10443
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10444
+ const params = {};
10445
+ const result = await client.call("GET", "/blog-posts", params, void 0, void 0);
10446
+ const rows = tableRowsOf(result, "posts");
10447
+ emit(flags, result ?? {}, () => printTable(rows, listBlogPostsColumns));
10448
+ });
10449
+ blog.command("create").summary("Create a blog post").description(
10450
+ "Creates a blog post as a DRAFT (or published with `publish: true` when it has a body). The body is Markdown. A slug the caller sets must be free (409) and not `feed`; a slug derived from the title takes the next free numeric suffix. Returns the full post. Requires the blog early access on the workspace (403 otherwise)."
10451
+ ).requiredOption("--title <value>", "Post title (max 200 characters).").option(
10452
+ "--slug <value>",
10453
+ "Derived from the title when omitted; a taken derived slug gets a numeric suffix."
10454
+ ).option("--content <value>", "The body as Markdown (headings, lists, links, images by URL).").option(
10455
+ "--excerpt <value>",
10456
+ "Short plain-text summary (list, RSS, meta description); derived from the body on publish when empty."
10457
+ ).option("--cover <value>", "Cover image URL (Media Library).").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option("--seo <json|file>", "seo", parseJsonObjectFileOrInline("--seo")).option("--publish", "Publish right away (needs a non-empty body); default false = draft.").option("--no-publish", "Send `publish: false` explicitly.").action(async (options, command) => {
10458
+ const flags = getGlobalFlags(command);
10459
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10460
+ const params = {};
10461
+ const payload = {
10462
+ title: options.title,
10463
+ slug: options.slug,
10464
+ content: options.content,
10465
+ excerpt: options.excerpt,
10466
+ coverImageUrl: options.cover,
10467
+ tags: options.tags,
10468
+ seo: options.seo,
10469
+ publish: options.publish
10470
+ };
10471
+ const result = await client.call("POST", "/blog-posts", params, void 0, payload);
10472
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10473
+ });
10474
+ blog.command("get").summary("Blog post detail").description(
10475
+ "One blog post with its Markdown body and SEO overrides (title, description, noindex). 404 when unknown."
10476
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").action(async (id, _options, command) => {
10477
+ const flags = getGlobalFlags(command);
10478
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10479
+ const params = {
10480
+ postId: id
10481
+ };
10482
+ const result = await client.call("GET", "/blog-posts/:postId", params, void 0, void 0);
10483
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10484
+ });
10485
+ blog.command("update").summary("Update a blog post").description(
10486
+ "Partial update of a blog post: title, slug (must be free, 409), Markdown body, excerpt, cover (null removes it), tags, SEO (whole section replaced). A published post keeps serving the new content at once. Returns the full post."
10487
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--title <value>", "title").option(
10488
+ "--slug <value>",
10489
+ "Public path segment (`/blog/{slug}`): lowercase letters, digits and hyphens."
10490
+ ).option("--content <value>", "The body as Markdown (headings, lists, links, images by URL).").option(
10491
+ "--excerpt <value>",
10492
+ "Short plain-text summary (list, RSS, meta description); derived from the body on publish when empty."
10493
+ ).option("--cover <value>", "Cover image URL; null removes the cover.").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option(
10494
+ "--seo <json|file>",
10495
+ "Replaces the whole SEO section.",
10496
+ parseJsonObjectFileOrInline("--seo")
10497
+ ).action(async (id, options, command) => {
10498
+ const flags = getGlobalFlags(command);
10499
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10500
+ const params = {
10501
+ postId: id
10502
+ };
10503
+ const payload = {
10504
+ title: options.title,
10505
+ slug: options.slug,
10506
+ content: options.content,
10507
+ excerpt: options.excerpt,
10508
+ coverImageUrl: options.cover,
10509
+ tags: options.tags,
10510
+ seo: options.seo
10511
+ };
10512
+ const result = await client.call("PATCH", "/blog-posts/:postId", params, void 0, payload);
10513
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10514
+ });
10515
+ blog.command("publish").summary("Publish or unpublish a blog post").description(
10516
+ "Publishes a post at `/blog/{slug}` (needs a non-empty body, 400 otherwise; the excerpt is derived from the body when empty) or takes it back to draft. The first publication date is the post's date and survives unpublish / republish. Reversible. Returns the full post."
10517
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--publish", "true = publish, false = back to draft.").action(async (id, options, command) => {
10518
+ const flags = getGlobalFlags(command);
10519
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10520
+ const params = {
10521
+ postId: id
10522
+ };
10523
+ const payload = {
10524
+ publish: options.publish
10525
+ };
10526
+ const result = await client.call(
10527
+ "POST",
10528
+ "/blog-posts/:postId/publish",
10529
+ params,
10530
+ void 0,
10531
+ payload
10532
+ );
10533
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10534
+ });
10535
+ blog.command("delete").summary("Delete a blog post").description(
10536
+ "Deletes a blog post for good; a published one disappears from `/blog` at once. No undo - prefer unpublishing."
10537
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--confirm", "required \u2014 acknowledge that you want to deletes the post for good").action(async (id, options, command) => {
10538
+ const flags = getGlobalFlags(command);
10539
+ requireConfirm(options.confirm, "deletes the post for good");
10540
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10541
+ const params = {
10542
+ postId: id
10543
+ };
10544
+ const result = await client.call(
10545
+ "DELETE",
10546
+ "/blog-posts/:postId",
10547
+ params,
10548
+ void 0,
10549
+ void 0
10550
+ );
10551
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10552
+ });
10553
+ }
10554
+
10352
10555
  // src/commands/_generated/index.ts
10353
10556
  function registerGeneratedCommands(program2) {
10354
10557
  registerCertificatesCommands(program2);
10558
+ registerBlogCommands(program2);
10355
10559
  }
10356
10560
 
10357
10561
  // src/commands/ads/index.ts
@@ -12103,6 +12307,9 @@ var RECIPIENT_STATUSES = [
12103
12307
  "complained",
12104
12308
  "clicked"
12105
12309
  ];
12310
+ function collectRepeatable(value, previous) {
12311
+ return [...previous, value];
12312
+ }
12106
12313
  function parsePositiveInt2(value, flag) {
12107
12314
  const n = Number.parseInt(value, 10);
12108
12315
  if (!Number.isInteger(n) || n <= 0) {
@@ -12140,13 +12347,15 @@ function resolveSender(options) {
12140
12347
  };
12141
12348
  }
12142
12349
  function resolveTheme(options) {
12143
- if (options.backgroundColor === void 0 && options.contentBackgroundColor === void 0 && options.linkColor === void 0) {
12350
+ if (options.backgroundColor === void 0 && options.contentBackgroundColor === void 0 && options.linkColor === void 0 && options.buttonColor === void 0 && options.buttonTextColor === void 0) {
12144
12351
  return void 0;
12145
12352
  }
12146
12353
  return {
12147
12354
  ...options.backgroundColor !== void 0 ? { backgroundColor: options.backgroundColor } : {},
12148
12355
  ...options.contentBackgroundColor !== void 0 ? { contentBackgroundColor: options.contentBackgroundColor } : {},
12149
- ...options.linkColor !== void 0 ? { linkColor: options.linkColor } : {}
12356
+ ...options.linkColor !== void 0 ? { linkColor: options.linkColor } : {},
12357
+ ...options.buttonColor !== void 0 ? { buttonColor: options.buttonColor } : {},
12358
+ ...options.buttonTextColor !== void 0 ? { buttonTextColor: options.buttonTextColor } : {}
12150
12359
  };
12151
12360
  }
12152
12361
  function resolveAudience(options) {
@@ -12259,7 +12468,10 @@ function registerCampaignsCommands(program2) {
12259
12468
  ).option("--template-id <id>", "look template the Markdown body renders into").option("--from-name <name>", "From display name").option("--reply-to <email>", "Reply-To address (requires --from-name)").option(
12260
12469
  "--from-email <email>",
12261
12470
  "custom From on the workspace's verified sending domain (requires --from-name); omit for the platform sender"
12262
- ).option("--segment <segmentId>", "send to a saved audience segment (re-resolved at send time)").option("--all-subscribed", "send to every subscribed client (the default audience)").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").action(
12471
+ ).option("--segment <segmentId>", "send to a saved audience segment (re-resolved at send time)").option("--all-subscribed", "send to every subscribed client (the default audience)").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").option(
12472
+ "--button-color <hex>",
12473
+ "theme: background of buttons without their own color (wins over the look default)"
12474
+ ).option("--button-text-color <hex>", "theme: text color of buttons without their own color").action(
12263
12475
  async (options, command) => {
12264
12476
  const flags = getGlobalFlags(command);
12265
12477
  const contentMarkdown = resolveContent(options);
@@ -12287,7 +12499,10 @@ function registerCampaignsCommands(program2) {
12287
12499
  });
12288
12500
  }
12289
12501
  );
12290
- campaigns.command("update").description("Update a DRAFT campaign (campaigns past draft are immutable except cancel)").argument("<id>", "campaign id").option("--name <name>", "new internal label").option("--subject <subject>", "new email subject line").option("--preheader <text>", "new preview text").option("--content <markdown>", "new campaign body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new campaign body (Markdown) from a file").option("--html-file <path>", "read the new HTML document of a designed e-mail from a file").option("--template-id <id>", "look template for the body (`none` = the default shell)").option("--from-name <name>", "new From display name").option("--reply-to <email>", "new Reply-To address (requires --from-name)").option("--from-email <email>", "new custom From address (requires --from-name)").option("--segment <segmentId>", "switch the audience to a saved segment").option("--all-subscribed", "switch the audience to every subscribed client").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").action(
12502
+ campaigns.command("update").description("Update a DRAFT campaign (campaigns past draft are immutable except cancel)").argument("<id>", "campaign id").option("--name <name>", "new internal label").option("--subject <subject>", "new email subject line").option("--preheader <text>", "new preview text").option("--content <markdown>", "new campaign body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new campaign body (Markdown) from a file").option("--html-file <path>", "read the new HTML document of a designed e-mail from a file").option("--template-id <id>", "look template for the body (`none` = the default shell)").option("--from-name <name>", "new From display name").option("--reply-to <email>", "new Reply-To address (requires --from-name)").option("--from-email <email>", "new custom From address (requires --from-name)").option("--segment <segmentId>", "switch the audience to a saved segment").option("--all-subscribed", "switch the audience to every subscribed client").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").option(
12503
+ "--button-color <hex>",
12504
+ "theme: background of buttons without their own color (wins over the look default)"
12505
+ ).option("--button-text-color <hex>", "theme: text color of buttons without their own color").action(
12291
12506
  async (id, options, command) => {
12292
12507
  const flags = getGlobalFlags(command);
12293
12508
  const input = {};
@@ -12330,6 +12545,25 @@ function registerCampaignsCommands(program2) {
12330
12545
  `);
12331
12546
  });
12332
12547
  });
12548
+ campaigns.command("test").description(
12549
+ "Send a TEST of the saved campaign to up to 3 addresses (default: your own) \u2014 subject prefixed [Test], sample personalization values; never the audience"
12550
+ ).argument("<id>", "campaign id").option(
12551
+ "--to <email>",
12552
+ "test recipient \u2014 repeatable or comma-separated, max 3 (default: your own address)",
12553
+ collectRepeatable,
12554
+ []
12555
+ ).action(async (id, options, command) => {
12556
+ const flags = getGlobalFlags(command);
12557
+ const recipients = options.to.flatMap((entry) => entry.split(",")).map((email) => email.trim()).filter(Boolean);
12558
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12559
+ const result = await client.sendEmailCampaignTest(id, recipients);
12560
+ emit(flags, result, () => {
12561
+ process.stdout.write(
12562
+ `Test of campaign ${id} queued \u2192 ${result.recipients.join(", ")} (subject prefixed [Test]). Nothing was sent to the audience.
12563
+ `
12564
+ );
12565
+ });
12566
+ });
12333
12567
  campaigns.command("send").description(
12334
12568
  "\u26A0\uFE0F SENDS the campaign \u2014 REAL bulk email to the entire resolved audience, IRREVERSIBLY. Delivered mail cannot be recalled; `campaigns cancel` only stops remaining sends. Requires --confirm. Omit --at to send NOW; --at schedules (max 30 days out)."
12335
12569
  ).argument("<id>", "campaign id").option(
@@ -12339,7 +12573,7 @@ function registerCampaignsCommands(program2) {
12339
12573
  const flags = getGlobalFlags(command);
12340
12574
  if (!options.confirm) {
12341
12575
  throw new Error(
12342
- `Refusing to send without --confirm. \`campaigns send\` emails the ENTIRE resolved audience for real and cannot be undone once mail is delivered. Re-run with --confirm to proceed (check the draft first with \`campaigns get ${id}\`).`
12576
+ `Refusing to send without --confirm. \`campaigns send\` emails the ENTIRE resolved audience for real and cannot be undone once mail is delivered. Re-run with --confirm to proceed (check the draft first with \`campaigns get ${id}\` and \`campaigns test ${id}\`).`
12343
12577
  );
12344
12578
  }
12345
12579
  if (options.at !== void 0 && Number.isNaN(Date.parse(options.at))) {
@@ -12374,6 +12608,28 @@ function registerCampaignsCommands(program2) {
12374
12608
  const { stats } = result.campaign;
12375
12609
  process.stdout.write(
12376
12610
  `Cancelled campaign ${result.campaign.id} \u2014 ${stats.sent} recipient${stats.sent === 1 ? " was" : "s were"} already sent to before the cancel.
12611
+ `
12612
+ );
12613
+ });
12614
+ });
12615
+ campaigns.command("resend").description(
12616
+ "Follow up a SENT campaign: builds a segment over its non-openers (or non-clickers) and creates a DRAFT copy aimed at it - nothing is sent; review and `campaigns send` the draft"
12617
+ ).argument("<id>", "campaign id (must be sent)").option("--target <target>", "not-opened (default) | not-clicked").action(async (id, options, command) => {
12618
+ const flags = getGlobalFlags(command);
12619
+ let target;
12620
+ if (options.target !== void 0) {
12621
+ if (options.target !== "not-opened" && options.target !== "not-clicked") {
12622
+ process.stderr.write("--target must be not-opened or not-clicked\n");
12623
+ process.exitCode = 1;
12624
+ return;
12625
+ }
12626
+ target = options.target;
12627
+ }
12628
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12629
+ const result = await client.resendEmailCampaign(id, target);
12630
+ emit(flags, result, () => {
12631
+ process.stdout.write(
12632
+ `Draft ${result.campaign.id} "${result.campaign.name}" created for segment "${result.segment.name}" (${result.segment.id}${result.segmentReused ? ", reused" : ""}). Nothing was sent - review it, then \`campaigns send <id> --confirm\`.
12377
12633
  `
12378
12634
  );
12379
12635
  });
@@ -13341,7 +13597,7 @@ function parsePositiveInt4(value, flag) {
13341
13597
  }
13342
13598
  return n;
13343
13599
  }
13344
- function collectRepeatable(value, previous) {
13600
+ function collectRepeatable2(value, previous) {
13345
13601
  return [...previous, value];
13346
13602
  }
13347
13603
  function parseCsv(raw) {
@@ -13825,7 +14081,7 @@ Next page: --cursor ${page.nextCursor}
13825
14081
  ).argument("<csvFile>", "path to the CSV file").option(
13826
14082
  "--tag <tagId>",
13827
14083
  "existing tag id applied to every imported row (repeat for several; unknown ids fail)",
13828
- collectRepeatable,
14084
+ collectRepeatable2,
13829
14085
  []
13830
14086
  ).action(async (csvFile, options, command) => {
13831
14087
  const flags = getGlobalFlags(command);
@@ -13836,6 +14092,7 @@ Next page: --cursor ${page.nextCursor}
13836
14092
  let created = 0;
13837
14093
  let existing = 0;
13838
14094
  let invalid = 0;
14095
+ let overLimit = 0;
13839
14096
  for (let offset = 0; offset < contacts.length; offset += IMPORT_CHUNK_SIZE) {
13840
14097
  const chunk2 = contacts.slice(offset, offset + IMPORT_CHUNK_SIZE);
13841
14098
  if (!flags.json && contacts.length > IMPORT_CHUNK_SIZE) {
@@ -13849,13 +14106,18 @@ Next page: --cursor ${page.nextCursor}
13849
14106
  created += page.created;
13850
14107
  existing += page.existing;
13851
14108
  invalid += page.invalid;
14109
+ overLimit += page.overLimit ?? 0;
13852
14110
  }
13853
- const summary = { results, created, existing, invalid };
14111
+ const summary = { results, created, existing, invalid, overLimit };
13854
14112
  emit(flags, summary, () => {
13855
14113
  process.stdout.write(
13856
- `Imported ${contacts.length} row${contacts.length === 1 ? "" : "s"} from ${csvFile}: ${created} created, ${existing} already existed, ${invalid} invalid.
13857
- `
14114
+ `Imported ${contacts.length} row${contacts.length === 1 ? "" : "s"} from ${csvFile}: ${created} created, ${existing} already existed, ${invalid} invalid` + (overLimit > 0 ? `, ${overLimit} over the plan's contacts limit` : "") + ".\n"
13858
14115
  );
14116
+ if (overLimit > 0) {
14117
+ process.stdout.write(
14118
+ "The plan's contacts limit is reached \u2014 upgrade the plan to import the remaining rows.\n"
14119
+ );
14120
+ }
13859
14121
  const problems = results.filter((r) => r.status !== "created" && r.status !== "exists");
13860
14122
  if (problems.length > 0) {
13861
14123
  process.stdout.write("\nRows that were not imported:\n");
@@ -14945,11 +15207,11 @@ Next page: --cursor ${result.nextCursor}
14945
15207
  member.command("mute").description("Mute (timeout) a member for N minutes (1..40320 = 28 days)").argument("<communityId>", "community id").argument("<memberId>", "member id (wcid)").requiredOption("--minutes <n>", "mute duration in minutes (1..40320)").action(
14946
15208
  async (communityId, memberId, options, command) => {
14947
15209
  const flags = getGlobalFlags(command);
14948
- const minutes = Number.parseInt(options.minutes, 10);
14949
- if (!Number.isInteger(minutes) || minutes < 1 || minutes > 40320) {
15210
+ const minutes2 = Number.parseInt(options.minutes, 10);
15211
+ if (!Number.isInteger(minutes2) || minutes2 < 1 || minutes2 > 40320) {
14950
15212
  throw new Error(`Invalid --minutes: ${options.minutes} (expected an integer 1..40320).`);
14951
15213
  }
14952
- const result = await clientFor(flags).muteCommunityMember(communityId, memberId, minutes);
15214
+ const result = await clientFor(flags).muteCommunityMember(communityId, memberId, minutes2);
14953
15215
  emit(flags, result, () => {
14954
15216
  process.stdout.write(`Muted member ${memberId} until ${result.mutedUntil}.
14955
15217
  `);
@@ -15737,7 +15999,7 @@ function assertStatus(status) {
15737
15999
  throw new Error(`Invalid status "${status}". Use one of: ${LESSON_STATUSES.join(", ")}`);
15738
16000
  }
15739
16001
  }
15740
- function collectRepeatable2(value, previous) {
16002
+ function collectRepeatable3(value, previous) {
15741
16003
  return [...previous, value];
15742
16004
  }
15743
16005
  function parseNonNegativeInt(value, flag) {
@@ -16345,7 +16607,7 @@ ${NOT_SAVED_NOTE} --moments=${options.out ?? "<file>"} --confirm\`.
16345
16607
  ).argument("<productId>", "course product id").argument("<lessonId>", "quiz lesson id").option("--type <type>", "single | multi | truefalse").option("--question <text>", "the question prompt").option("--explanation <text>", "explanation shown on the results screen").option("--points <n>", "points awarded (default 1)").option(
16346
16608
  "--option <text>",
16347
16609
  "an answer option (repeat for several; choice types only)",
16348
- collectRepeatable2,
16610
+ collectRepeatable3,
16349
16611
  []
16350
16612
  ).option("--correct <indices>", "1-based, comma-separated indices of the correct option(s)").option("--correct-answer <bool>", "true/false questions: the correct answer").option("--json <fileOrJson>", "full QuizQuestionInput object as inline JSON or a .json path").action(
16351
16613
  async (productId, lessonId, options, command) => {
@@ -16361,7 +16623,7 @@ ${NOT_SAVED_NOTE} --moments=${options.out ?? "<file>"} --confirm\`.
16361
16623
  quiz.command("update").description("Replace a question in full (same flags as `quiz add`, or --json)").argument("<productId>", "course product id").argument("<lessonId>", "quiz lesson id").argument("<questionId>", "question id").option("--type <type>", "single | multi | truefalse").option("--question <text>", "the question prompt").option("--explanation <text>", "explanation shown on the results screen").option("--points <n>", "points awarded (default 1)").option(
16362
16624
  "--option <text>",
16363
16625
  "an answer option (repeat for several; choice types only)",
16364
- collectRepeatable2,
16626
+ collectRepeatable3,
16365
16627
  []
16366
16628
  ).option("--correct <indices>", "1-based, comma-separated indices of the correct option(s)").option("--correct-answer <bool>", "true/false questions: the correct answer").option("--json <fileOrJson>", "full QuizQuestionInput object as inline JSON or a .json path").action(
16367
16629
  async (productId, lessonId, questionId, options, command) => {
@@ -16545,7 +16807,7 @@ function parseIsoDate(value, flag) {
16545
16807
  }
16546
16808
  return value;
16547
16809
  }
16548
- function collectRepeatable3(value, previous) {
16810
+ function collectRepeatable4(value, previous) {
16549
16811
  return [...previous, value];
16550
16812
  }
16551
16813
  function registerDiscountsCommands(program2) {
@@ -16570,7 +16832,7 @@ function registerDiscountsCommands(program2) {
16570
16832
  discounts2.command("create").description("Create a discount code (exactly one of --percent-off / --amount-off)").requiredOption("--code <code>", "discount code (e.g. LAUNCH20)").option("--percent-off <pct>", "percentage discount (> 0, <= 100)").option("--amount-off <minorUnits>", "fixed discount in minor units (e.g. 1000 = 10.00)").option("--currency <currency>", "ISO currency code (required with --amount-off)").option(
16571
16833
  "--product <id>",
16572
16834
  "limit to a product (repeat the flag for several)",
16573
- collectRepeatable3,
16835
+ collectRepeatable4,
16574
16836
  []
16575
16837
  ).option("--max-redemptions <n>", "maximum number of redemptions").option("--ends <isoDate>", "date the code stops working (ISO, e.g. 2026-12-31)").option("--starts <isoDate>", "date the code starts working (ISO, e.g. 2026-12-01)").option("--forever", "apply to every renewal of a subscription (default: first payment only)").option("--inactive", "create the code deactivated (default: active)").action(
16576
16838
  async (options, command) => {
@@ -16620,7 +16882,7 @@ function registerDiscountsCommands(program2) {
16620
16882
  discounts2.command("update").description("Update a discount code (value, products, limits, dates, active state)").argument("<discountId>", "discount id").option("--percent-off <pct>", "switch to a percentage discount (> 0, <= 100)").option("--amount-off <minorUnits>", "switch to a fixed discount in minor units").option("--currency <currency>", "ISO currency code for --amount-off").option(
16621
16883
  "--product <id>",
16622
16884
  "replace the product restriction (repeat the flag for several)",
16623
- collectRepeatable3,
16885
+ collectRepeatable4,
16624
16886
  []
16625
16887
  ).option("--max-redemptions <n>", "maximum number of redemptions").option("--clear-max-redemptions", "remove the redemption limit").option("--ends <isoDate>", "date the code stops working (ISO, e.g. 2026-12-31)").option("--clear-ends", "remove the end date").option("--forever", "apply to every renewal of a subscription").option("--active", "activate the code").option("--inactive", "deactivate the code").action(
16626
16888
  async (discountId, options, command) => {
@@ -17285,6 +17547,10 @@ function printSignupFormDetail(form) {
17285
17547
  `);
17286
17548
  process.stdout.write(` Redirect: ${form.redirectUrl ?? "(success state on the form)"}
17287
17549
  `);
17550
+ process.stdout.write(
17551
+ ` After confirmation: ${form.confirmationRedirectUrl ?? "(platform confirmation page)"}
17552
+ `
17553
+ );
17288
17554
  if (form.confirmationEmail) {
17289
17555
  process.stdout.write(
17290
17556
  ` Confirmation email overrides: subject ${form.confirmationEmail.subject ?? "(default)"}, intro ${form.confirmationEmail.intro ?? "(default)"}
@@ -17342,6 +17608,9 @@ async function buildFormUpdateInput(client, formId, options) {
17342
17608
  input.optInMode = parseChoice2(options.optIn, OPT_IN_MODES, "--opt-in");
17343
17609
  }
17344
17610
  if (options.redirect !== void 0) input.redirectUrl = orNull3(options.redirect);
17611
+ if (options.confirmationRedirect !== void 0) {
17612
+ input.confirmationRedirectUrl = orNull3(options.confirmationRedirect);
17613
+ }
17345
17614
  let current;
17346
17615
  const fetchCurrent = async () => {
17347
17616
  current ??= await client.getSignupForm(formId);
@@ -17404,7 +17673,10 @@ function registerFormsCommands(program2) {
17404
17673
  });
17405
17674
  forms.command("create").description(
17406
17675
  "Create a standalone signup form (double opt-in unless --opt-in single). The full confirmation-email copy is set with `forms update --json`"
17407
- ).requiredOption("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids applied on (confirmed) submission").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")}; default double)`).option("--redirect <url>", "thank-you redirect after a successful submit").option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option("--phone <mode>", `config: phone number field (${PHONE_FIELD_MODES.join(" | ")})`).option(
17676
+ ).requiredOption("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids applied on (confirmed) submission").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")}; default double)`).option("--redirect <url>", "thank-you redirect after a successful submit").option(
17677
+ "--confirmation-redirect <url>",
17678
+ "where the confirmation e-mail click lands (double opt-in): lead magnet, OTO, calendar"
17679
+ ).option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option("--phone <mode>", `config: phone number field (${PHONE_FIELD_MODES.join(" | ")})`).option(
17408
17680
  "--sms-consent <on|off>",
17409
17681
  "config: marketing-SMS consent checkbox (needs the phone field)"
17410
17682
  ).option(
@@ -17422,7 +17694,8 @@ function registerFormsCommands(program2) {
17422
17694
  tagIds: options.tags !== void 0 ? parseCsv2(options.tags) : void 0,
17423
17695
  config,
17424
17696
  ...optInMode !== void 0 ? { optInMode } : {},
17425
- ...options.redirect !== void 0 ? { redirectUrl: options.redirect } : {}
17697
+ ...options.redirect !== void 0 ? { redirectUrl: options.redirect } : {},
17698
+ ...options.confirmationRedirect !== void 0 ? { confirmationRedirectUrl: options.confirmationRedirect } : {}
17426
17699
  });
17427
17700
  emit(flags, result, () => {
17428
17701
  process.stdout.write(
@@ -17434,7 +17707,10 @@ function registerFormsCommands(program2) {
17434
17707
  );
17435
17708
  forms.command("update").description(
17436
17709
  "Update a standalone form. The embed config is REPLACED IN FULL by the API \u2014 flat config flags are merged over the current config for you; a --json `config` object is sent as the full replacement (flat flags still win). `none` clears nullable fields."
17437
- ).argument("<id>", "form id").option("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids (full replace; empty string clears)").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")})`).option("--redirect <url|none>", "thank-you redirect after submit; `none` clears it").option("--confirmation-subject <text>", "double-opt-in confirmation email subject override").option("--confirmation-intro <text>", "double-opt-in confirmation email intro override").option("--clear-confirmation-email", "restore the workspace default confirmation copy").option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option("--phone <mode>", `config: phone number field (${PHONE_FIELD_MODES.join(" | ")})`).option(
17710
+ ).argument("<id>", "form id").option("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids (full replace; empty string clears)").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")})`).option("--redirect <url|none>", "thank-you redirect after submit; `none` clears it").option(
17711
+ "--confirmation-redirect <url|none>",
17712
+ "where the confirmation e-mail click lands (double opt-in); `none` restores the platform page"
17713
+ ).option("--confirmation-subject <text>", "double-opt-in confirmation email subject override").option("--confirmation-intro <text>", "double-opt-in confirmation email intro override").option("--clear-confirmation-email", "restore the workspace default confirmation copy").option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option("--phone <mode>", `config: phone number field (${PHONE_FIELD_MODES.join(" | ")})`).option(
17438
17714
  "--sms-consent <on|off>",
17439
17715
  "config: marketing-SMS consent checkbox (needs the phone field)"
17440
17716
  ).option(
@@ -19018,6 +19294,78 @@ join again to refresh it.
19018
19294
  });
19019
19295
  }
19020
19296
 
19297
+ // src/commands/liveRooms/index.ts
19298
+ var minutes = (seconds) => `${Math.round(seconds / 60)} min`;
19299
+ var percent = (part, whole) => whole > 0 ? `${Math.round(part / whole * 100)}%` : "-";
19300
+ function describeSchedule(room) {
19301
+ if (!room.schedule) return "fixed";
19302
+ const until = room.schedule.until ? ` until ${room.schedule.until}` : "";
19303
+ return `every ${room.schedule.everyMinutes} min${until}`;
19304
+ }
19305
+ function registerLiveRoomsCommands(program2) {
19306
+ const liveRooms = program2.command("live-rooms").description(
19307
+ "Live rooms (webinar rooms, incl. rolling autowebinar sessions) \u2014 list and attendance; rooms are managed in the dashboard"
19308
+ );
19309
+ liveRooms.command("list").description("List live rooms (latest start first)").action(async (_, command) => {
19310
+ const flags = getGlobalFlags(command);
19311
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19312
+ const result = await client.listLiveRooms();
19313
+ emit(flags, result, () => {
19314
+ printTable(
19315
+ result.rooms.map((room) => ({
19316
+ id: room.id,
19317
+ title: room.title,
19318
+ source: room.source,
19319
+ status: room.endedAt ? "ended" : room.status,
19320
+ startAt: room.startAt,
19321
+ schedule: describeSchedule(room),
19322
+ access: room.access.everyone ? "everyone" : "restricted"
19323
+ })),
19324
+ ["id", "title", "source", "status", "startAt", "schedule", "access"]
19325
+ );
19326
+ });
19327
+ });
19328
+ liveRooms.command("attendance").description("A room's attendance per session; --session adds that session's viewers").argument("<roomId>", "live room id (from live-rooms list)").option("--session <iso>", "ISO start of one session to list its attendees").action(async (roomId, options, command) => {
19329
+ const flags = getGlobalFlags(command);
19330
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19331
+ const result = await client.getLiveRoomAttendance(roomId, options.session);
19332
+ emit(flags, result, () => {
19333
+ printTable(
19334
+ result.sessions.map((session) => ({
19335
+ session: session.sessionStartAt,
19336
+ attendees: session.attendees,
19337
+ identified: session.identified,
19338
+ avgWatched: `${minutes(session.averageWatchedSeconds)} (${percent(
19339
+ session.averageWatchedSeconds,
19340
+ session.durationSeconds
19341
+ )})`,
19342
+ toEnd: `${session.watchedToEnd} (${percent(session.watchedToEnd, session.attendees)})`,
19343
+ sawOffer: session.sawOffer,
19344
+ clickedOffer: session.clickedOffer
19345
+ })),
19346
+ ["session", "attendees", "identified", "avgWatched", "toEnd", "sawOffer", "clickedOffer"]
19347
+ );
19348
+ if (options.session !== void 0) {
19349
+ process.stdout.write(`
19350
+ Viewers of ${options.session}:
19351
+ `);
19352
+ printTable(
19353
+ result.attendees.map((attendee) => ({
19354
+ who: attendee.displayName ?? attendee.email ?? attendee.attendee,
19355
+ email: attendee.email ?? "",
19356
+ clientId: attendee.clientId ?? "",
19357
+ watched: minutes(attendee.watchedSeconds),
19358
+ reached: minutes(attendee.maxPositionSeconds),
19359
+ firstSeenAt: attendee.firstSeenAt,
19360
+ offer: attendee.purchasedAt ? "bought" : attendee.offerClickedAt ? "clicked" : attendee.offerSeenAt ? "saw" : ""
19361
+ })),
19362
+ ["who", "email", "clientId", "watched", "reached", "firstSeenAt", "offer"]
19363
+ );
19364
+ }
19365
+ });
19366
+ });
19367
+ }
19368
+
19021
19369
  // src/commands/media/index.ts
19022
19370
  import { existsSync, readFileSync as readFileSync17, writeFileSync as writeFileSync4 } from "fs";
19023
19371
  import { readFile, stat as stat3 } from "fs/promises";
@@ -22419,6 +22767,100 @@ function registerMediaCommands(program2) {
22419
22767
  });
22420
22768
  }
22421
22769
  );
22770
+ const captions = media.command("captions").description("Caption (subtitle) tracks of a video - one per language");
22771
+ captions.command("list").description("List a video's caption tracks (language, source, status)").argument("<mediaId>", "video media item id").action(async (mediaId, _options, command) => {
22772
+ const flags = getGlobalFlags(command);
22773
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22774
+ const result = await client.listMediaCaptions(mediaId);
22775
+ emit(flags, result, () => {
22776
+ if (result.tracks.length === 0) {
22777
+ process.stdout.write(`No caption tracks on ${mediaId}.
22778
+ `);
22779
+ return;
22780
+ }
22781
+ printTable(
22782
+ result.tracks.map((track) => ({
22783
+ lang: track.srclang,
22784
+ label: track.label,
22785
+ source: track.source,
22786
+ status: track.recorded ? track.status : "not recorded",
22787
+ updated: track.updatedAt ?? ""
22788
+ })),
22789
+ ["lang", "label", "source", "status", "updated"]
22790
+ );
22791
+ });
22792
+ });
22793
+ captions.command("upload").description(
22794
+ "Add or replace the subtitles in one language from a .vtt or .srt file (UTF-8, up to 2 MB). The transcript's own language is refused - edit the transcript instead."
22795
+ ).argument("<mediaId>", "video media item id").requiredOption("--lang <code>", "language code of the track, e.g. en, de, pt-br").requiredOption("--file <path>", ".vtt or .srt file").option("--label <label>", "track name in the player's menu (default: the language name)").action(
22796
+ async (mediaId, options, command) => {
22797
+ const flags = getGlobalFlags(command);
22798
+ if (!existsSync(options.file)) {
22799
+ process.stderr.write(`File not found: ${options.file}
22800
+ `);
22801
+ process.exitCode = 1;
22802
+ return;
22803
+ }
22804
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22805
+ const result = await client.uploadMediaCaption(mediaId, {
22806
+ srclang: options.lang,
22807
+ content: readFileSync17(options.file, "utf8"),
22808
+ ...options.label ? { label: options.label } : {}
22809
+ });
22810
+ emit(flags, result, () => {
22811
+ process.stdout.write(
22812
+ result.track.status === "failed" ? `Stored the ${result.track.srclang} file, but the player refused it - run the same command again in a moment.
22813
+ ` : `Captions ${result.track.srclang} (${result.track.label}) are live on ${mediaId}.
22814
+ `
22815
+ );
22816
+ });
22817
+ }
22818
+ );
22819
+ captions.command("download").description("Print (or save) one caption track as WebVTT").argument("<mediaId>", "video media item id").requiredOption("--lang <code>", "language code of the track").option("--out <file>", "write the .vtt to a file instead of stdout").action(async (mediaId, options, command) => {
22820
+ const flags = getGlobalFlags(command);
22821
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22822
+ const result = await client.getMediaCaption(mediaId, options.lang);
22823
+ if (options.out) {
22824
+ writeFileSync4(options.out, result.content, "utf8");
22825
+ emit(flags, { mediaId, srclang: result.srclang, out: options.out }, () => {
22826
+ process.stdout.write(`Wrote the ${result.srclang} captions to ${options.out}.
22827
+ `);
22828
+ });
22829
+ return;
22830
+ }
22831
+ emit(flags, result, () => {
22832
+ process.stdout.write(result.content);
22833
+ });
22834
+ });
22835
+ captions.command("delete").description("Remove an uploaded caption track (it disappears from the player)").argument("<mediaId>", "video media item id").requiredOption("--lang <code>", "language code of the track").option("--confirm", "required: removes the track from the live player").action(
22836
+ async (mediaId, options, command) => {
22837
+ const flags = getGlobalFlags(command);
22838
+ if (!options.confirm) {
22839
+ process.stderr.write(
22840
+ "Refusing to delete without --confirm (the track disappears from the player).\n"
22841
+ );
22842
+ process.exitCode = 1;
22843
+ return;
22844
+ }
22845
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22846
+ const result = await client.deleteMediaCaption(mediaId, options.lang);
22847
+ emit(flags, result, () => {
22848
+ process.stdout.write(`Removed the ${result.srclang} captions from ${mediaId}.
22849
+ `);
22850
+ });
22851
+ }
22852
+ );
22853
+ captions.command("regenerate").description("Push the transcript's caption track to the player again").argument("<mediaId>", "video media item id").action(async (mediaId, _options, command) => {
22854
+ const flags = getGlobalFlags(command);
22855
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22856
+ const result = await client.regenerateMediaCaption(mediaId);
22857
+ emit(flags, result, () => {
22858
+ process.stdout.write(
22859
+ result.track.status === "failed" ? "The player refused the transcript captions - try again in a moment.\n" : `Transcript captions (${result.track.srclang}) pushed to the player.
22860
+ `
22861
+ );
22862
+ });
22863
+ });
22422
22864
  media.command("transcript").description(
22423
22865
  "Print a video transcript (plain text, or full JSON with --words / --json). Videos without a transcript yet report so; non-video media is a 404."
22424
22866
  ).argument("<mediaId>", "video media item id").option("--words", "include word-level timings (large payload; implies JSON output)").option("--out <file>", "write the transcript to a file instead of stdout").action(
@@ -22595,6 +23037,7 @@ var ACCESS_STRATEGIES = [
22595
23037
  "from_subscribe_date"
22596
23038
  ];
22597
23039
  var DOUBLE_OPT_INS = ["none", "external"];
23040
+ var DELIVERIES = ["zanfia", "integration"];
22598
23041
  function assertOneOf2(value, allowed, flag) {
22599
23042
  if (!allowed.includes(value)) {
22600
23043
  throw new Error(`Invalid ${flag}: ${value}. Use one of: ${allowed.join(", ")}`);
@@ -22630,7 +23073,7 @@ function parseJsonFileOrInline5(value, flag) {
22630
23073
  throw new Error(`Invalid JSON for ${flag}: ${error.message}`);
22631
23074
  }
22632
23075
  }
22633
- function collectRepeatable4(value, previous) {
23076
+ function collectRepeatable5(value, previous) {
22634
23077
  return [...previous, value];
22635
23078
  }
22636
23079
  function registerNewslettersCommands(program2) {
@@ -22652,9 +23095,10 @@ function registerNewslettersCommands(program2) {
22652
23095
  slug: issue.slug,
22653
23096
  status: issue.status,
22654
23097
  landing: issue.landingPageVisibility ?? "",
23098
+ send: issue.sendStatus ?? "",
22655
23099
  createdAt: issue.createdAt
22656
23100
  })),
22657
- ["id", "subject", "slug", "status", "landing", "createdAt"]
23101
+ ["id", "subject", "slug", "status", "landing", "send", "createdAt"]
22658
23102
  );
22659
23103
  if (page.nextCursor) {
22660
23104
  process.stdout.write(`
@@ -22838,6 +23282,90 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
22838
23282
  });
22839
23283
  }
22840
23284
  );
23285
+ issues.command("send").description(
23286
+ "\u26A0\uFE0F SENDS the issue from Zanfia to every subscriber of the newsletter product \u2014 REAL bulk e-mail, IRREVERSIBLE once delivered (`issues cancel` only stops the remaining sends). Sending is publishing: a staged draft is promoted first. The newsletter must send from Zanfia (`settings update --delivery zanfia`). Requires --confirm. Omit --at to send NOW; --at schedules (max 30 days out)."
23287
+ ).argument("<productId>", "newsletter product id").argument("<issueId>", "issue id").option(
23288
+ "--at <isoTimestamp>",
23289
+ "schedule the send (ISO, max 30 days out) instead of sending now"
23290
+ ).option("--confirm", "required \u2014 acknowledge this e-mails the subscribers for real").action(
23291
+ async (productId, issueId, options, command) => {
23292
+ const flags = getGlobalFlags(command);
23293
+ if (!options.confirm) {
23294
+ throw new Error(
23295
+ `Refusing to send without --confirm. \`newsletters issues send\` e-mails EVERY subscriber of the newsletter for real and cannot be undone once mail is delivered. Re-run with --confirm to proceed (check the issue first with \`newsletters issues list --product ${productId}\`).`
23296
+ );
23297
+ }
23298
+ if (options.at !== void 0 && Number.isNaN(Date.parse(options.at))) {
23299
+ throw new Error(
23300
+ `Invalid --at timestamp: ${options.at} (expected ISO, e.g. 2026-07-15T09:00:00Z)`
23301
+ );
23302
+ }
23303
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23304
+ const result = await client.sendNewsletterIssue(productId, issueId, {
23305
+ ...options.at !== void 0 ? { scheduledAt: options.at } : {}
23306
+ });
23307
+ emit(flags, result, () => {
23308
+ const { send } = result;
23309
+ if (send.scheduledAt) {
23310
+ process.stdout.write(
23311
+ `Issue ${result.issueId} scheduled for ${send.scheduledAt} \u2014 stop it before then with \`newsletters issues cancel ${productId} ${issueId} --confirm\`.
23312
+ `
23313
+ );
23314
+ } else {
23315
+ process.stdout.write(
23316
+ `Issue ${result.issueId} is sending now (status: ${send.status}). \`newsletters issues cancel\` can only stop the remaining sends.
23317
+ `
23318
+ );
23319
+ }
23320
+ });
23321
+ }
23322
+ );
23323
+ issues.command("cancel").description(
23324
+ "Cancel a scheduled or in-flight issue send \u2014 remaining sends stop; delivered mail is out. Requires --confirm."
23325
+ ).argument("<productId>", "newsletter product id").argument("<issueId>", "issue id").option("--confirm", "required \u2014 acknowledge the send is stopped").action(
23326
+ async (productId, issueId, options, command) => {
23327
+ const flags = getGlobalFlags(command);
23328
+ if (!options.confirm) {
23329
+ throw new Error(
23330
+ "Refusing to cancel without --confirm. Re-run with --confirm to un-queue the scheduled send or stop the one in flight."
23331
+ );
23332
+ }
23333
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23334
+ const result = await client.cancelNewsletterIssueSend(productId, issueId);
23335
+ emit(flags, result, () => {
23336
+ process.stdout.write(
23337
+ result.send ? `Stopped the send of issue ${result.issueId} (status: ${result.send.status}).
23338
+ ` : `Un-queued the send of issue ${result.issueId}; it is a draft again.
23339
+ `
23340
+ );
23341
+ });
23342
+ }
23343
+ );
23344
+ issues.command("report").description("Progress and delivery / engagement stats of an issue\u2019s native send").argument("<productId>", "newsletter product id").argument("<issueId>", "issue id").action(async (productId, issueId, command) => {
23345
+ const flags = getGlobalFlags(command);
23346
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23347
+ const result = await client.getNewsletterIssueSendReport(productId, issueId);
23348
+ emit(flags, result, () => {
23349
+ if (!result.send) {
23350
+ process.stdout.write(`Issue ${result.issueId} was never sent from Zanfia.
23351
+ `);
23352
+ return;
23353
+ }
23354
+ const { send, report } = result;
23355
+ process.stdout.write(
23356
+ `Issue ${result.issueId}: ${send.status}` + (send.scheduledAt ? ` (scheduled for ${send.scheduledAt})` : "") + (send.recipientCount !== null ? `, ${send.recipientCount} recipients` : "") + "\n"
23357
+ );
23358
+ if (report) {
23359
+ printTable(
23360
+ Object.entries(report.stats).map(([stat4, value]) => ({
23361
+ stat: stat4,
23362
+ value: String(value ?? "")
23363
+ })),
23364
+ ["stat", "value"]
23365
+ );
23366
+ }
23367
+ });
23368
+ });
22841
23369
  issues.command("delete").description(
22842
23370
  "\u26A0\uFE0F PERMANENTLY delete an archived issue \u2014 a HARD delete with no undo. Requires --confirm."
22843
23371
  ).argument("<issueId>", "archived issue id").requiredOption("--product <id>", "newsletter product id").option("--confirm", "required \u2014 acknowledge the issue is removed permanently").action(
@@ -22856,9 +23384,11 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
22856
23384
  });
22857
23385
  }
22858
23386
  );
22859
- const settings = newsletters.command("settings").description("Edit newsletter settings (archive, archive inbox, double opt-in, client tags)");
23387
+ const settings = newsletters.command("settings").description(
23388
+ "Edit newsletter settings (archive, archive inbox, double opt-in, client tags, delivery, sender)"
23389
+ );
22860
23390
  settings.command("update").description(
22861
- "Patch newsletter settings. Archive flags merge per-field; --archive-inbox, --double-opt-in and --client-tag replace their whole block. At least one change is required."
23391
+ "Patch newsletter settings. Archive and sender flags merge per-field; --archive-inbox, --double-opt-in and --client-tag replace their whole block. At least one change is required."
22862
23392
  ).requiredOption("--product <id>", "newsletter product id").option("--archive-enabled <on|off>", "archive: enable/disable the public archive").option("--archive-access <strategy>", `archive: ${ACCESS_STRATEGIES.join("|")}`).option(
22863
23393
  "--publication-delay <days|none>",
22864
23394
  'archive: days before a new issue goes public; "none" = immediately'
@@ -22874,11 +23404,20 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
22874
23404
  ).option("--double-opt-in <mode>", `subscription confirmation (${DOUBLE_OPT_INS.join("|")})`).option(
22875
23405
  "--client-tag <tag>",
22876
23406
  "tag applied to new subscribers (repeat the flag; replaces the list)",
22877
- collectRepeatable4,
23407
+ collectRepeatable5,
22878
23408
  []
22879
- ).action(
23409
+ ).option(
23410
+ "--delivery <mode>",
23411
+ `who sends the issues: ${DELIVERIES.join("|")} (zanfia = native sending from the product)`
23412
+ ).option(
23413
+ "--from-name <name>",
23414
+ 'sender: From display name of native sends ("" = workspace default)'
23415
+ ).option("--reply-to <email>", 'sender: Reply-To of native sends ("" = workspace default)').option("--clear-sender", "sender: drop both defaults (workspace defaults apply)").action(
22880
23416
  async (options, command) => {
22881
23417
  const flags = getGlobalFlags(command);
23418
+ if (options.clearSender && (options.fromName !== void 0 || options.replyTo !== void 0)) {
23419
+ throw new Error("Use either --clear-sender or --from-name/--reply-to, not both.");
23420
+ }
22882
23421
  const archive = {};
22883
23422
  if (options.archiveEnabled !== void 0) {
22884
23423
  archive.enabled = parseBool2(options.archiveEnabled, "--archive-enabled");
@@ -22922,6 +23461,17 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
22922
23461
  input.doubleOptIn = assertOneOf2(options.doubleOptIn, DOUBLE_OPT_INS, "--double-opt-in");
22923
23462
  }
22924
23463
  if (options.clientTag.length > 0) input.clientTags = options.clientTag;
23464
+ if (options.delivery !== void 0) {
23465
+ input.delivery = assertOneOf2(options.delivery, DELIVERIES, "--delivery");
23466
+ }
23467
+ if (options.clearSender) {
23468
+ input.sender = null;
23469
+ } else if (options.fromName !== void 0 || options.replyTo !== void 0) {
23470
+ input.sender = {
23471
+ ...options.fromName !== void 0 ? { fromName: options.fromName || null } : {},
23472
+ ...options.replyTo !== void 0 ? { replyTo: options.replyTo || null } : {}
23473
+ };
23474
+ }
22925
23475
  if (Object.keys(input).length === 0) {
22926
23476
  throw new Error(
22927
23477
  "Nothing to update \u2014 pass at least one settings flag (see `newsletters settings update --help`)."
@@ -24544,6 +25094,22 @@ import { readFile as readFile3 } from "fs/promises";
24544
25094
  import { basename as basename2 } from "path";
24545
25095
  var KB_TREE_CHANGED_ERROR = "tree-changed";
24546
25096
  var JSON_FLAG_HINT3 = "(write it as --json=<value> \u2014 a bare --json is the global JSON-output flag)";
25097
+ var PRODUCT_TYPE_INPUTS = [
25098
+ "downloads",
25099
+ "download",
25100
+ "newsletter",
25101
+ "course",
25102
+ "mailing",
25103
+ "community",
25104
+ "membership",
25105
+ "knowledge_base",
25106
+ "one_time",
25107
+ "custom",
25108
+ "event",
25109
+ "consultation",
25110
+ "service",
25111
+ "bundle"
25112
+ ];
24547
25113
  function parseCoverRef(value) {
24548
25114
  const separator = value.indexOf(":");
24549
25115
  if (separator < 0) {
@@ -24654,7 +25220,7 @@ function parseJsonFileOrInline8(value, flag) {
24654
25220
  throw new Error(`Invalid JSON for ${flag}: ${error.message}`);
24655
25221
  }
24656
25222
  }
24657
- function collectRepeatable5(value, previous) {
25223
+ function collectRepeatable6(value, previous) {
24658
25224
  return [...previous, value];
24659
25225
  }
24660
25226
  var HTTP_URL_PATTERN2 = /^https?:\/\//i;
@@ -24742,7 +25308,10 @@ function registerProductsCommands(program2) {
24742
25308
  });
24743
25309
  products2.command("create").description(
24744
25310
  "Create a new product. Community/newsletter types seed their full content (community: channels doc, built-in roles, owner member). Add --free or --amount to also seed a default offer \u2014 e.g. a paid community: --type community --amount 49.99 --currency usd --period monthly"
24745
- ).requiredOption("--name <name>", "product name").requiredOption("--type <type>", "product type (course, newsletter, community, ...)").option("--description <description>", "product description").option("--free", "seed a free default offer").option("--amount <amount>", "seed a paid default offer \u2014 price in MAJOR units (49.99)").option("--currency <currency>", "currency for --amount (pln, usd, eur, gbp)").option(
25311
+ ).requiredOption("--name <name>", "product name").requiredOption(
25312
+ "--type <type>",
25313
+ `product type: ${PRODUCT_TYPE_INPUTS.join(", ")} (downloads/download are stored as one_time, newsletter as mailing)`
25314
+ ).option("--description <description>", "product description").option("--free", "seed a free default offer").option("--amount <amount>", "seed a paid default offer \u2014 price in MAJOR units (49.99)").option("--currency <currency>", "currency for --amount (pln, usd, eur, gbp)").option(
24746
25315
  "--period <period>",
24747
25316
  "billing period for --amount: one_time, monthly or yearly (default one_time)"
24748
25317
  ).option("--description-file <path>", "read the product description from a file").option(
@@ -24751,7 +25320,7 @@ function registerProductsCommands(program2) {
24751
25320
  ).option(
24752
25321
  "--file <path>",
24753
25322
  "local file to upload and attach to the product (downloads products; repeat for several)",
24754
- collectRepeatable5,
25323
+ collectRepeatable6,
24755
25324
  []
24756
25325
  ).action(
24757
25326
  async (options, command) => {
@@ -25086,7 +25655,7 @@ function registerProductsCommands(program2) {
25086
25655
  ).argument("<productId>", "appointment product id").option("--config <fileOrJson>", "full config object as inline JSON or a path to a .json file").option("--booking-method <method>", `consultation: ${BOOKING_METHODS.join("|")}`).option("--booking-url <url>", "consultation: external scheduler URL").option("--duration <minutes>", "consultation: session length in minutes").option("--location <text>", "consultation location / event join URL or address").option("--location-details <text>", "consultation: extra location details").option("--confirmation-message <text>", "consultation: post-booking confirmation message").option(
25087
25656
  "--deliverable <text>",
25088
25657
  "service: a deliverable line (repeat the flag; replaces the list)",
25089
- collectRepeatable5,
25658
+ collectRepeatable6,
25090
25659
  []
25091
25660
  ).option("--delivery-time <n>", "service: delivery time value").option("--delivery-unit <unit>", `service: ${DELIVERY_UNITS.join("|")} (with --delivery-time)`).option("--revision-rounds <n|unlimited>", 'service: revision rounds count or "unlimited"').option("--brief-method <method>", `service: ${BRIEF_METHODS.join("|")}`).option("--brief-url <url>", "service: external brief/intake form URL").option("--event-mode <mode>", `event: ${EVENT_MODES3.join("|")}`).option("--starts <iso>", "event: start (ISO 8601)").option("--ends <iso>", "event: end (ISO 8601)").option("--capacity <n>", "event: attendee capacity").option("--show-on-success <on|off>", "show the post-purchase CTA on the success page").action(
25092
25661
  async (productId, options, command) => {
@@ -25282,7 +25851,7 @@ function registerProductsCommands(program2) {
25282
25851
  ).argument("<productId>", "product id").requiredOption(
25283
25852
  "--image <url>",
25284
25853
  "an image URL (repeat the flag for several; the set fully replaces the current images)",
25285
- collectRepeatable5,
25854
+ collectRepeatable6,
25286
25855
  []
25287
25856
  ).action(async (productId, options, command) => {
25288
25857
  const flags = getGlobalFlags(command);
@@ -26011,7 +26580,7 @@ Failed deliveries (${result.failedDeliveries.length})
26011
26580
  // src/commands/segments/index.ts
26012
26581
  import { readFileSync as readFileSync24 } from "fs";
26013
26582
  var SUBSCRIPTION_STATUSES2 = ["pending", "subscribed", "unsubscribed"];
26014
- function collectRepeatable6(value, previous) {
26583
+ function collectRepeatable7(value, previous) {
26015
26584
  return [...previous, value];
26016
26585
  }
26017
26586
  function parseMinorUnits(value, flag) {
@@ -26091,48 +26660,54 @@ function resolveConditions(options) {
26091
26660
  }
26092
26661
  conditions.push({ field: "emailSubscription", operator: "is", status });
26093
26662
  }
26663
+ if (options.notOpened !== void 0) {
26664
+ conditions.push({ field: "campaign", operator: "not-opened", campaignId: options.notOpened });
26665
+ }
26666
+ if (options.notClicked !== void 0) {
26667
+ conditions.push({ field: "campaign", operator: "not-clicked", campaignId: options.notClicked });
26668
+ }
26094
26669
  return conditions;
26095
26670
  }
26096
26671
  function withConditionOptions(command) {
26097
26672
  return command.option(
26098
26673
  "--has-tag <tagId>",
26099
26674
  "client carries this tag (repeat for several \u2014 conditions are ANDed)",
26100
- collectRepeatable6,
26675
+ collectRepeatable7,
26101
26676
  []
26102
26677
  ).option(
26103
26678
  "--without-tag <tagId>",
26104
26679
  "client does NOT carry this tag (repeatable)",
26105
- collectRepeatable6,
26680
+ collectRepeatable7,
26106
26681
  []
26107
26682
  ).option(
26108
26683
  "--has-any-tag <tagId>",
26109
26684
  "client carries AT LEAST ONE of these tags (repeat the flag; one OR condition, max 10 ids) - one send to several tags without double-mailing",
26110
- collectRepeatable6,
26685
+ collectRepeatable7,
26111
26686
  []
26112
26687
  ).option(
26113
26688
  "--has-none-tag <tagId>",
26114
26689
  "client carries NONE of these tags (repeat the flag; one condition, max 10 ids)",
26115
- collectRepeatable6,
26690
+ collectRepeatable7,
26116
26691
  []
26117
26692
  ).option(
26118
26693
  "--owns <productId>",
26119
26694
  "client has ACTIVE access to this product - a direct grant OR through a bundle (repeatable)",
26120
- collectRepeatable6,
26695
+ collectRepeatable7,
26121
26696
  []
26122
26697
  ).option(
26123
26698
  "--not-owns <productId>",
26124
26699
  "client has NO active access to this product, direct or through a bundle (repeatable)",
26125
- collectRepeatable6,
26700
+ collectRepeatable7,
26126
26701
  []
26127
26702
  ).option(
26128
26703
  "--owns-direct <productId>",
26129
26704
  "client has an ACTIVE grant on this product itself (bought / granted by hand) - bundle access does not count; use to mail only the buyers of that one product (repeatable)",
26130
- collectRepeatable6,
26705
+ collectRepeatable7,
26131
26706
  []
26132
26707
  ).option(
26133
26708
  "--not-owns-direct <productId>",
26134
26709
  "client has NO active direct grant on this product - bundle holders DO match (repeatable)",
26135
- collectRepeatable6,
26710
+ collectRepeatable7,
26136
26711
  []
26137
26712
  ).option(
26138
26713
  "--spent-gte <minorUnits>",
@@ -26140,6 +26715,12 @@ function withConditionOptions(command) {
26140
26715
  ).option("--spent-lte <minorUnits>", "lifetime spend at most this (minor units)").option(
26141
26716
  "--subscription <status>",
26142
26717
  `marketing-email subscription state is (${SUBSCRIPTION_STATUSES2.join(" | ")})`
26718
+ ).option(
26719
+ "--not-opened <campaignId>",
26720
+ "received this SENT campaign and never opened it (the resend-to-non-openers audience)"
26721
+ ).option(
26722
+ "--not-clicked <campaignId>",
26723
+ "received this SENT campaign and never clicked a link in it"
26143
26724
  ).option(
26144
26725
  "--json-file <path>",
26145
26726
  "read the conditions as a JSON array from a file instead of flags"
@@ -26166,6 +26747,8 @@ function describeCondition(condition) {
26166
26747
  return `sms consent is ${condition.status}`;
26167
26748
  case "phoneNumber":
26168
26749
  return condition.operator === "has" ? "has a phone number" : "has no phone number";
26750
+ case "campaign":
26751
+ return `${condition.operator.replace("-", " ")} campaign ${condition.campaignId}`;
26169
26752
  }
26170
26753
  }
26171
26754
  function registerSegmentsCommands(program2) {
@@ -28474,6 +29057,10 @@ function registerWorkflowAuthoringCommands(workflows) {
28474
29057
  ).option(
28475
29058
  "--trigger <eventType>",
28476
29059
  "business event that starts it, e.g. product.ProductPurchased (see `workflows list` for examples)"
29060
+ ).option(
29061
+ "--kind <kind>",
29062
+ "transactional (default for purchase/access triggers: no unsubscribe link, reaches everyone) or marketing (unsubscribe link + headers, e-mail/SMS steps skipped for unsubscribed contacts); omitted = derived from the triggers at publish",
29063
+ parseWorkflowKind
28477
29064
  ).action(
28478
29065
  async (options, command) => {
28479
29066
  const flags = getGlobalFlags(command);
@@ -28481,6 +29068,7 @@ function registerWorkflowAuthoringCommands(workflows) {
28481
29068
  if (options.description !== void 0) input.description = options.description;
28482
29069
  if (options.product !== void 0) input.productIds = options.product;
28483
29070
  if (options.trigger !== void 0) input.triggerEventType = options.trigger;
29071
+ if (options.kind !== void 0) input.kind = options.kind;
28484
29072
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
28485
29073
  const result = await client.createAutomation(input);
28486
29074
  emit(flags, result, () => {
@@ -28535,6 +29123,10 @@ function registerWorkflowAuthoringCommands(workflows) {
28535
29123
  "--product <ids>",
28536
29124
  "also replace the product scope (comma-separated ids)",
28537
29125
  parseCommaList("--product")
29126
+ ).option(
29127
+ "--kind <kind>",
29128
+ "also set the kind: transactional or marketing (see `workflows create --help`)",
29129
+ parseWorkflowKind
28538
29130
  ).action(
28539
29131
  async (automationId, options, command) => {
28540
29132
  const flags = getGlobalFlags(command);
@@ -28543,7 +29135,8 @@ function registerWorkflowAuthoringCommands(workflows) {
28543
29135
  draft: options.json,
28544
29136
  ...options.name !== void 0 ? { name: options.name } : {},
28545
29137
  ...options.description !== void 0 ? { description: options.description } : {},
28546
- ...options.product !== void 0 ? { productIds: options.product } : {}
29138
+ ...options.product !== void 0 ? { productIds: options.product } : {},
29139
+ ...options.kind !== void 0 ? { kind: options.kind } : {}
28547
29140
  }).catch(withGraphHint);
28548
29141
  emit(flags, result, () => {
28549
29142
  if (result.issues.length > 0) {
@@ -28656,12 +29249,13 @@ function registerWorkflowsCommands(program2) {
28656
29249
  id: w.id,
28657
29250
  name: w.name,
28658
29251
  status: w.status,
29252
+ kind: w.kind,
28659
29253
  triggers: w.triggers.join(", "),
28660
29254
  products: formatProductScope(w),
28661
29255
  runsThisMonth: w.runsThisMonth,
28662
29256
  lastRun: w.lastRunAt ?? ""
28663
29257
  })),
28664
- ["id", "name", "status", "triggers", "products", "runsThisMonth", "lastRun"]
29258
+ ["id", "name", "status", "kind", "triggers", "products", "runsThisMonth", "lastRun"]
28665
29259
  );
28666
29260
  });
28667
29261
  });
@@ -28797,6 +29391,8 @@ function printWorkflowSummary(workflow) {
28797
29391
  if (workflow.description) process.stdout.write(` Description: ${workflow.description}
28798
29392
  `);
28799
29393
  process.stdout.write(` Status: ${workflow.status}
29394
+ `);
29395
+ process.stdout.write(` Kind: ${workflow.kind}
28800
29396
  `);
28801
29397
  process.stdout.write(` Triggers: ${workflow.triggers.join(", ")}
28802
29398
  `);
@@ -28833,6 +29429,7 @@ var GRAPH_ACTION_LABELS = {
28833
29429
  createCommunityPost: "Create community post",
28834
29430
  updateClientTags: "Update client tags",
28835
29431
  setEmailSubscription: "Set email subscription",
29432
+ openClientDeadline: "Open deadline window",
28836
29433
  requestTestimonial: "Request testimonial"
28837
29434
  };
28838
29435
  var GRAPH_NODE_KINDS = /* @__PURE__ */ new Set([
@@ -29069,6 +29666,12 @@ function describeAction(node, templateNames) {
29069
29666
  const sub = isRecord2(node.config.emailSubscription) ? node.config.emailSubscription : {};
29070
29667
  return `${label} \u2014 ${optionalString(sub.status) ?? "?"}`;
29071
29668
  }
29669
+ case "openClientDeadline": {
29670
+ const deadline = isRecord2(node.config.clientDeadline) ? node.config.clientDeadline : {};
29671
+ const amount = typeof deadline.amount === "number" ? String(deadline.amount) : "?";
29672
+ const endTime = optionalString(deadline.endTime);
29673
+ return `${label} \u2014 ${optionalString(deadline.key) ?? "?"} for ${amount} ${optionalString(deadline.unit) ?? "?"}${endTime ? `, closes at ${endTime}` : ""}`;
29674
+ }
29072
29675
  case "requestTestimonial":
29073
29676
  return label;
29074
29677
  }
@@ -29108,7 +29711,11 @@ function describeDelay(config) {
29108
29711
  switch (mode) {
29109
29712
  case void 0:
29110
29713
  case "duration":
29111
- return `wait ${typeof config.amount === "number" ? config.amount : "?"} ${optionalString(config.unit) ?? ""}`.trimEnd();
29714
+ return `wait ${typeof config.amount === "number" ? config.amount : "?"} ${optionalString(config.unit) ?? ""}${optionalString(config.atTime) ? `, then at ${config.atTime}` : ""}`.trimEnd();
29715
+ case "untilTime": {
29716
+ const weekdays = Array.isArray(config.weekdays) ? config.weekdays.join(",") : "";
29717
+ return `wait until the next ${optionalString(config.time) ?? "?"}${weekdays ? ` on weekdays ${weekdays}` : ""}${optionalString(config.timezone) ? ` (${config.timezone})` : ""}`;
29718
+ }
29112
29719
  case "untilWeekday":
29113
29720
  return `wait until weekday ${typeof config.weekday === "number" ? config.weekday : "?"} at ${optionalString(config.time) ?? "?"}${optionalString(config.timezone) ? ` (${config.timezone})` : ""}`;
29114
29721
  case "untilDate":
@@ -29275,8 +29882,8 @@ function applyShortHelp(program2) {
29275
29882
 
29276
29883
  // src/program.ts
29277
29884
  function resolveCliVersion() {
29278
- if ("0.3.52".length > 0) {
29279
- return "0.3.52";
29885
+ if ("0.4.0".length > 0) {
29886
+ return "0.4.0";
29280
29887
  }
29281
29888
  try {
29282
29889
  const raw = readFileSync28(new URL("../package.json", import.meta.url), "utf8");
@@ -29337,6 +29944,7 @@ Signing in \u2014 two roles, two consent pages:
29337
29944
  registerSmsCampaignsCommands(program2);
29338
29945
  registerSegmentsCommands(program2);
29339
29946
  registerFormsCommands(program2);
29947
+ registerLiveRoomsCommands(program2);
29340
29948
  registerTestimonialsCommands(program2);
29341
29949
  registerBrandCommands(program2);
29342
29950
  registerTeamCommands(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zanfia/cli",
3
- "version": "0.3.52",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "Zanfia CLI — thin HTTP client over the Zanfia Public API",
6
6
  "type": "module",