@zanfia/cli 0.3.26 → 0.3.47

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 +1962 -306
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6900,6 +6900,9 @@ var {
6900
6900
  Help
6901
6901
  } = import_index.default;
6902
6902
 
6903
+ // src/program.ts
6904
+ import { readFileSync as readFileSync26 } from "fs";
6905
+
6903
6906
  // src/utils/config.ts
6904
6907
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
6905
6908
  import { homedir } from "os";
@@ -7629,6 +7632,15 @@ function createMockClient() {
7629
7632
  async whoami() {
7630
7633
  return mockBackend.whoami();
7631
7634
  },
7635
+ async getWorkspaceSettings() {
7636
+ throw new ApiError(501, "workspace settings are not available in mock mode");
7637
+ },
7638
+ async updateWorkspaceSettings() {
7639
+ throw new ApiError(501, "workspace settings are not available in mock mode");
7640
+ },
7641
+ async listBundledAccessClients() {
7642
+ return { items: [] };
7643
+ },
7632
7644
  async listProducts() {
7633
7645
  return mockBackend.listProducts();
7634
7646
  },
@@ -7776,6 +7788,18 @@ function createMockClient() {
7776
7788
  async listClientTags() {
7777
7789
  return { count: 0, tags: [] };
7778
7790
  },
7791
+ async createClientTag() {
7792
+ throw new ApiError(501, "client tag writes are not available in mock mode");
7793
+ },
7794
+ async updateClientTag() {
7795
+ throw new ApiError(501, "client tag writes are not available in mock mode");
7796
+ },
7797
+ async deleteClientTag() {
7798
+ throw new ApiError(501, "client tag writes are not available in mock mode");
7799
+ },
7800
+ async bulkUpdateClientTagAssignments() {
7801
+ throw new ApiError(501, "client tag writes are not available in mock mode");
7802
+ },
7779
7803
  async getClientEmailLog() {
7780
7804
  throw new ApiError(501, "the email delivery log is not available in mock mode");
7781
7805
  },
@@ -7821,6 +7845,24 @@ function createMockClient() {
7821
7845
  async listAutomations() {
7822
7846
  return { items: [] };
7823
7847
  },
7848
+ async getAutomationGraph() {
7849
+ throw new ApiError(501, "workflow authoring is not available in mock mode");
7850
+ },
7851
+ async createAutomation() {
7852
+ throw new ApiError(501, "workflow authoring is not available in mock mode");
7853
+ },
7854
+ async updateAutomationDraft() {
7855
+ throw new ApiError(501, "workflow authoring is not available in mock mode");
7856
+ },
7857
+ async publishAutomation() {
7858
+ throw new ApiError(501, "workflow authoring is not available in mock mode");
7859
+ },
7860
+ async duplicateAutomation() {
7861
+ throw new ApiError(501, "workflow authoring is not available in mock mode");
7862
+ },
7863
+ async deleteAutomation() {
7864
+ throw new ApiError(501, "workflow authoring is not available in mock mode");
7865
+ },
7824
7866
  async getAutomation() {
7825
7867
  throw new ApiError(501, "workflows are not available in mock mode");
7826
7868
  },
@@ -8207,6 +8249,81 @@ function createMockClient() {
8207
8249
  async listCampaignRecipients() {
8208
8250
  throw new ApiError(501, "email campaigns are not available in mock mode");
8209
8251
  },
8252
+ async listCampaignLinks() {
8253
+ throw new ApiError(501, "email campaigns are not available in mock mode");
8254
+ },
8255
+ async listSmsCampaigns() {
8256
+ return { campaigns: [] };
8257
+ },
8258
+ async getSmsCampaign() {
8259
+ throw new ApiError(501, "SMS campaigns are not available in mock mode");
8260
+ },
8261
+ async createSmsCampaign() {
8262
+ throw new ApiError(501, "SMS campaigns are not available in mock mode");
8263
+ },
8264
+ async updateSmsCampaign() {
8265
+ throw new ApiError(501, "SMS campaigns are not available in mock mode");
8266
+ },
8267
+ async deleteSmsCampaign() {
8268
+ throw new ApiError(501, "SMS campaigns are not available in mock mode");
8269
+ },
8270
+ async sendSmsCampaign() {
8271
+ throw new ApiError(501, "SMS campaigns are not available in mock mode");
8272
+ },
8273
+ async cancelSmsCampaign() {
8274
+ throw new ApiError(501, "SMS campaigns are not available in mock mode");
8275
+ },
8276
+ async listSmsCampaignRecipients() {
8277
+ throw new ApiError(501, "SMS campaigns are not available in mock mode");
8278
+ },
8279
+ async listLibraryEmails() {
8280
+ return { emails: [] };
8281
+ },
8282
+ async getLibraryEmail() {
8283
+ throw new ApiError(501, "the e-mail library is not available in mock mode");
8284
+ },
8285
+ async createLibraryEmail() {
8286
+ throw new ApiError(501, "the e-mail library is not available in mock mode");
8287
+ },
8288
+ async updateLibraryEmail() {
8289
+ throw new ApiError(501, "the e-mail library is not available in mock mode");
8290
+ },
8291
+ async duplicateLibraryEmail() {
8292
+ throw new ApiError(501, "the e-mail library is not available in mock mode");
8293
+ },
8294
+ async deleteLibraryEmail() {
8295
+ throw new ApiError(501, "the e-mail library is not available in mock mode");
8296
+ },
8297
+ async getSenderDomain() {
8298
+ throw new ApiError(501, "sender settings are not available in mock mode");
8299
+ },
8300
+ async updateSenderPostalAddress() {
8301
+ throw new ApiError(501, "sender settings are not available in mock mode");
8302
+ },
8303
+ async listWorkspaceClientEmails() {
8304
+ return { emails: [] };
8305
+ },
8306
+ async listClientEmails() {
8307
+ return { emails: [] };
8308
+ },
8309
+ async createClientEmailDraft() {
8310
+ throw new ApiError(501, "client e-mails are not available in mock mode");
8311
+ },
8312
+ async updateClientEmailDraft() {
8313
+ throw new ApiError(501, "client e-mails are not available in mock mode");
8314
+ },
8315
+ async deleteClientEmailDraft() {
8316
+ throw new ApiError(501, "client e-mails are not available in mock mode");
8317
+ },
8318
+ async sendClientEmail() {
8319
+ throw new ApiError(501, "client e-mails are not available in mock mode");
8320
+ },
8321
+ async bulkSetClientsEmailSubscription() {
8322
+ throw new ApiError(501, "bulk subscription changes are not available in mock mode");
8323
+ },
8324
+ async bulkSetClientsSmsSubscription() {
8325
+ throw new ApiError(501, "bulk subscription changes are not available in mock mode");
8326
+ },
8210
8327
  async listAudienceSegments() {
8211
8328
  return { count: 0, segments: [] };
8212
8329
  },
@@ -8787,6 +8904,9 @@ function createHttpClient(config) {
8787
8904
  };
8788
8905
  return {
8789
8906
  whoami: () => request("GET", "/workspace"),
8907
+ getWorkspaceSettings: () => request("GET", "/workspace"),
8908
+ updateWorkspaceSettings: (input) => request("PATCH", "/workspace", input),
8909
+ listBundledAccessClients: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/clients/bundled-access`),
8790
8910
  listProducts: () => request("GET", "/products"),
8791
8911
  getProduct: (id) => request("GET", `/products/${encodeURIComponent(id)}`),
8792
8912
  createProduct: (input) => request("POST", "/products", input),
@@ -8883,6 +9003,10 @@ function createHttpClient(config) {
8883
9003
  ),
8884
9004
  changeClientEmail: (id, newEmail) => request("POST", `/clients/${encodeURIComponent(id)}/change-email`, { newEmail }),
8885
9005
  listClientTags: () => request("GET", "/clients/tags"),
9006
+ createClientTag: (input) => request("POST", "/clients/tags", input),
9007
+ updateClientTag: (tagId, input) => request("PATCH", `/clients/tags/${encodeURIComponent(tagId)}`, input),
9008
+ deleteClientTag: (tagId) => request("DELETE", `/clients/tags/${encodeURIComponent(tagId)}`),
9009
+ bulkUpdateClientTagAssignments: (input) => request("POST", "/clients/tags/assignments", input),
8886
9010
  getClientEmailLog: (clientId, limit) => request(
8887
9011
  "GET",
8888
9012
  `/clients/${encodeURIComponent(clientId)}/email-log${limit !== void 0 ? `?limit=${limit}` : ""}`
@@ -8915,6 +9039,16 @@ function createHttpClient(config) {
8915
9039
  deleteDiscount: (id) => request("DELETE", `/discounts/${encodeURIComponent(id)}`),
8916
9040
  listAutomations: () => request("GET", "/automations"),
8917
9041
  getAutomation: (id) => request("GET", `/automations/${encodeURIComponent(id)}`),
9042
+ getAutomationGraph: (id) => request("GET", `/automations/${encodeURIComponent(id)}?include=graph`),
9043
+ createAutomation: (input) => request("POST", "/automations", input),
9044
+ updateAutomationDraft: (id, input) => request("PUT", `/automations/${encodeURIComponent(id)}/draft`, input),
9045
+ publishAutomation: (id) => request("POST", `/automations/${encodeURIComponent(id)}/publish`),
9046
+ duplicateAutomation: (id, name) => request(
9047
+ "POST",
9048
+ `/automations/${encodeURIComponent(id)}/duplicate`,
9049
+ name !== void 0 ? { name } : {}
9050
+ ),
9051
+ deleteAutomation: (id) => request("DELETE", `/automations/${encodeURIComponent(id)}`),
8918
9052
  listAutomationRuns: (id, limit) => request(
8919
9053
  "GET",
8920
9054
  `/automations/${encodeURIComponent(id)}/runs${limit !== void 0 ? `?limit=${limit}` : ""}`
@@ -9244,11 +9378,64 @@ function createHttpClient(config) {
9244
9378
  listCampaignRecipients: (id, options) => {
9245
9379
  const params = new URLSearchParams();
9246
9380
  if (options?.status) params.set("status", options.status);
9381
+ if (options?.linkId) params.set("linkId", options.linkId);
9247
9382
  if (options?.limit !== void 0) params.set("limit", String(options.limit));
9248
9383
  if (options?.cursor) params.set("cursor", options.cursor);
9249
9384
  const qs = params.toString();
9250
9385
  return request("GET", `/campaigns/${encodeURIComponent(id)}/recipients${qs ? `?${qs}` : ""}`);
9251
9386
  },
9387
+ listCampaignLinks: (id) => request("GET", `/campaigns/${encodeURIComponent(id)}/links`),
9388
+ // SMS campaigns — same lifecycle as e-mail campaigns, sent through the
9389
+ // workspace's own SMS provider account.
9390
+ listSmsCampaigns: () => request("GET", "/sms-campaigns"),
9391
+ getSmsCampaign: (id) => request("GET", `/sms-campaigns/${encodeURIComponent(id)}`),
9392
+ createSmsCampaign: (input) => request("POST", "/sms-campaigns", input),
9393
+ updateSmsCampaign: (id, input) => request("PATCH", `/sms-campaigns/${encodeURIComponent(id)}`, input),
9394
+ deleteSmsCampaign: (id) => request("DELETE", `/sms-campaigns/${encodeURIComponent(id)}`),
9395
+ sendSmsCampaign: (id, scheduledAt) => request(
9396
+ "POST",
9397
+ `/sms-campaigns/${encodeURIComponent(id)}/send`,
9398
+ scheduledAt !== void 0 ? { scheduledAt } : {}
9399
+ ),
9400
+ cancelSmsCampaign: (id) => request("POST", `/sms-campaigns/${encodeURIComponent(id)}/cancel`),
9401
+ listSmsCampaignRecipients: (id, options) => {
9402
+ const params = new URLSearchParams();
9403
+ if (options?.status) params.set("status", options.status);
9404
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
9405
+ if (options?.cursor) params.set("cursor", options.cursor);
9406
+ const qs = params.toString();
9407
+ return request(
9408
+ "GET",
9409
+ `/sms-campaigns/${encodeURIComponent(id)}/recipients${qs ? `?${qs}` : ""}`
9410
+ );
9411
+ },
9412
+ // Reusable e-mail templates (NOT the system e-mails under /email-templates).
9413
+ listLibraryEmails: () => request("GET", "/email-library"),
9414
+ getLibraryEmail: (id) => request("GET", `/email-library/${encodeURIComponent(id)}`),
9415
+ createLibraryEmail: (input) => request("POST", "/email-library", input),
9416
+ updateLibraryEmail: (id, input) => request("PATCH", `/email-library/${encodeURIComponent(id)}`, input),
9417
+ duplicateLibraryEmail: (id) => request("POST", `/email-library/${encodeURIComponent(id)}/duplicate`),
9418
+ deleteLibraryEmail: (id) => request("DELETE", `/email-library/${encodeURIComponent(id)}`),
9419
+ getSenderDomain: () => request("GET", "/sender-domain"),
9420
+ updateSenderPostalAddress: (postalAddress) => request("PUT", "/sender-domain/postal-address", { postalAddress }),
9421
+ listWorkspaceClientEmails: (limit) => request("GET", `/client-emails${limit !== void 0 ? `?limit=${limit}` : ""}`),
9422
+ listClientEmails: (clientId) => request("GET", `/clients/${encodeURIComponent(clientId)}/emails`),
9423
+ createClientEmailDraft: (clientId, input) => request("POST", `/clients/${encodeURIComponent(clientId)}/emails`, input),
9424
+ updateClientEmailDraft: (clientId, emailId, input) => request(
9425
+ "PATCH",
9426
+ `/clients/${encodeURIComponent(clientId)}/emails/${encodeURIComponent(emailId)}`,
9427
+ input
9428
+ ),
9429
+ deleteClientEmailDraft: (clientId, emailId) => request(
9430
+ "DELETE",
9431
+ `/clients/${encodeURIComponent(clientId)}/emails/${encodeURIComponent(emailId)}`
9432
+ ),
9433
+ sendClientEmail: (clientId, emailId) => request(
9434
+ "POST",
9435
+ `/clients/${encodeURIComponent(clientId)}/emails/${encodeURIComponent(emailId)}/send`
9436
+ ),
9437
+ bulkSetClientsEmailSubscription: (input) => request("POST", "/clients/email-subscription", input),
9438
+ bulkSetClientsSmsSubscription: (input) => request("POST", "/clients/sms-subscription", input),
9252
9439
  listAudienceSegments: () => request("GET", "/segments"),
9253
9440
  createAudienceSegment: (input) => request("POST", "/segments", input),
9254
9441
  updateAudienceSegment: (id, input) => request("PATCH", `/segments/${encodeURIComponent(id)}`, input),
@@ -9363,7 +9550,10 @@ function createHttpClient(config) {
9363
9550
  "DELETE",
9364
9551
  `/products/${encodeURIComponent(productId)}/kb/articles/${encodeURIComponent(articleId)}`
9365
9552
  ),
9366
- addKbCategory: (productId, name) => request("POST", `/products/${encodeURIComponent(productId)}/kb/categories`, { name }),
9553
+ addKbCategory: (productId, name, parentCategoryId) => request("POST", `/products/${encodeURIComponent(productId)}/kb/categories`, {
9554
+ name,
9555
+ ...parentCategoryId ? { parentCategoryId } : {}
9556
+ }),
9367
9557
  renameKbCategory: (productId, categoryId, name) => request(
9368
9558
  "PATCH",
9369
9559
  `/products/${encodeURIComponent(productId)}/kb/categories/${encodeURIComponent(categoryId)}`,
@@ -9821,6 +10011,9 @@ function registerCertificatesCommands(program2) {
9821
10011
  "--template <value>",
9822
10012
  "Explicit template id; omitted = the course template, else the workspace default."
9823
10013
  ).option(
10014
+ "--reissue-revoked",
10015
+ "Re-issue clients whose current certificate is REVOKED (new document in the current template, new number and verification link, the revoked one stays revoked). ACTIVE certificates are still skipped. Default false = any existing certificate skips the client."
10016
+ ).option("--no-reissue-revoked", "Send `reissueRevoked: false` explicitly.").option(
9824
10017
  "--confirm",
9825
10018
  "required \u2014 acknowledge that you want to issue certificates (recipients get an email)"
9826
10019
  ).action(async (productId, options, command) => {
@@ -9832,7 +10025,8 @@ function registerCertificatesCommands(program2) {
9832
10025
  productId,
9833
10026
  clientIds: options.client,
9834
10027
  onlyCompleted: options.onlyCompleted,
9835
- templateId: options.template
10028
+ templateId: options.template,
10029
+ reissueRevoked: options.reissueRevoked
9836
10030
  };
9837
10031
  const result = await client.call("POST", "/certificates/issue", params, void 0, payload);
9838
10032
  emit(flags, result ?? {}, () => printJson(result ?? {}));
@@ -9872,6 +10066,27 @@ function registerCertificatesCommands(program2) {
9872
10066
  );
9873
10067
  emit(flags, result ?? {}, () => printJson(result ?? {}));
9874
10068
  });
10069
+ certificates.command("delete").summary("Delete a revoked certificate").description(
10070
+ 'Permanent \u2014 removes a REVOKED certificate together with its PDF; there is NO undo. The public verification link then answers "not found" (instead of "revoked"), and the same client + course can be issued a fresh certificate afterwards. Only revoked certificates can be deleted \u2014 an active one is refused (revoke-certificate first). Addressed by the public credential id (`cert_\u2026`). Meant for test issuances that reached a real client.'
10071
+ ).argument("<certificateId>", "Public credential id (`cert_\u2026`) from list-certificates.").option(
10072
+ "--confirm",
10073
+ "required \u2014 acknowledge that you want to delete the revoked certificate (hard delete)"
10074
+ ).action(async (certificateId, options, command) => {
10075
+ const flags = getGlobalFlags(command);
10076
+ requireConfirm(options.confirm, "delete the revoked certificate (hard delete)");
10077
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10078
+ const params = {
10079
+ certificateId
10080
+ };
10081
+ const result = await client.call(
10082
+ "DELETE",
10083
+ "/certificates/:certificateId",
10084
+ params,
10085
+ void 0,
10086
+ void 0
10087
+ );
10088
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10089
+ });
9875
10090
  certificates.command("resend").summary("Resend the certificate email").description(
9876
10091
  'Queues the "certificate issued" delivery email (with the PDF link) again \u2014 for bounced or lost mail, or after a recipient-name correction. Active certificates only: a revoked certificate is a 400 (reinstate first), an unknown id a 404. It is a REAL email to the customer. Returns an empty object.'
9877
10092
  ).argument("<certificateId>", "Public credential id (`cert_\u2026`) from list-certificates.").option("--confirm", "required \u2014 acknowledge that you want to resend the certificate email").action(
@@ -11217,8 +11432,8 @@ function promptHidden(question) {
11217
11432
  stdin.setRawMode(true);
11218
11433
  stdin.resume();
11219
11434
  let value = "";
11220
- const onData = (chunk) => {
11221
- for (const char of chunk.toString("utf8")) {
11435
+ const onData = (chunk2) => {
11436
+ for (const char of chunk2.toString("utf8")) {
11222
11437
  if (char === "\r" || char === "\n") {
11223
11438
  stdin.setRawMode(false);
11224
11439
  stdin.pause();
@@ -11722,10 +11937,20 @@ function parsePositiveInt2(value, flag) {
11722
11937
  }
11723
11938
  return n;
11724
11939
  }
11940
+ function resolveTemplateId(options) {
11941
+ if (options.templateId === void 0) return void 0;
11942
+ return options.templateId === "none" ? null : options.templateId;
11943
+ }
11944
+ function resolveHtml(options) {
11945
+ return options.htmlFile !== void 0 ? readFileSync4(options.htmlFile, "utf8") : void 0;
11946
+ }
11725
11947
  function resolveContent(options) {
11726
11948
  if (options.content !== void 0 && options.contentFile !== void 0) {
11727
11949
  throw new Error("Use either --content or --content-file, not both.");
11728
11950
  }
11951
+ if (options.htmlFile !== void 0 && (options.content !== void 0 || options.contentFile !== void 0)) {
11952
+ throw new Error("Use either --html-file (designed e-mail) or a Markdown body, not both.");
11953
+ }
11729
11954
  return options.contentFile !== void 0 ? readFileSync4(options.contentFile, "utf8") : options.content;
11730
11955
  }
11731
11956
  function resolveSender(options) {
@@ -11762,6 +11987,26 @@ function resolveAudience(options) {
11762
11987
  function describeAudience(audience) {
11763
11988
  return audience.type === "segment" ? `segment ${audience.segmentId}` : "all subscribed clients";
11764
11989
  }
11990
+ function formatRate(value, base) {
11991
+ return base > 0 ? `${(Math.round(value / base * 1e3) / 10).toFixed(1)}%` : "n/a";
11992
+ }
11993
+ function printCampaignEngagement(stats) {
11994
+ const tracked = stats.trackedRecipients ?? 0;
11995
+ const uniqueOpens = stats.uniqueOpens ?? 0;
11996
+ const uniqueClicks = stats.uniqueClicks ?? 0;
11997
+ process.stdout.write(
11998
+ ` Engagement (tracked ${tracked} of ${stats.sent}): opens ${uniqueOpens} unique / ${stats.opens ?? 0} total (${formatRate(uniqueOpens, tracked)}, estimate), clicks ${uniqueClicks} unique / ${stats.clicks ?? 0} total (${formatRate(uniqueClicks, tracked)})
11999
+ `
12000
+ );
12001
+ const attributedOrders = stats.attributedOrders ?? 0;
12002
+ if (attributedOrders > 0) {
12003
+ const revenue = ((stats.attributedRevenue ?? 0) / 100).toFixed(2);
12004
+ process.stdout.write(
12005
+ ` Attributed: ${attributedOrders} order${attributedOrders === 1 ? "" : "s"}, ${revenue} ${stats.attributedCurrency ?? ""}
12006
+ `
12007
+ );
12008
+ }
12009
+ }
11765
12010
  function printCampaignDetail(campaign) {
11766
12011
  process.stdout.write(`Campaign ${campaign.id}
11767
12012
  `);
@@ -11794,10 +12039,13 @@ function printCampaignDetail(campaign) {
11794
12039
  ` Stats: sent ${stats.sent}, delivered ${stats.delivered}, bounced ${stats.bounced}, complained ${stats.complained}, failed ${stats.failed}, skipped ${stats.skippedSuppressed + stats.skippedMuted}
11795
12040
  `
11796
12041
  );
11797
- process.stdout.write(` Content (markdown, ${campaign.contentMarkdown.length} chars):
12042
+ printCampaignEngagement(stats);
12043
+ const isHtml = campaign.contentFormat === "html";
12044
+ const body = isHtml ? campaign.contentHtml ?? "" : campaign.contentMarkdown;
12045
+ process.stdout.write(` Content (${isHtml ? "html" : "markdown"}, ${body.length} chars):
11798
12046
  `);
11799
- if (campaign.contentMarkdown) {
11800
- for (const line of campaign.contentMarkdown.split("\n")) {
12047
+ if (body) {
12048
+ for (const line of body.split("\n")) {
11801
12049
  process.stdout.write(` ${line}
11802
12050
  `);
11803
12051
  }
@@ -11832,13 +12080,17 @@ function registerCampaignsCommands(program2) {
11832
12080
  });
11833
12081
  campaigns.command("create").description(
11834
12082
  "Create a DRAFT campaign \u2014 nothing is emailed; only --name is required at draft time (subject/content/sender are enforced by `campaigns send`)"
11835
- ).requiredOption("--name <name>", "internal label shown in the campaigns list (not the subject)").option("--subject <subject>", "email subject line").option("--preheader <text>", "preview text shown after the subject in inboxes").option("--content <markdown>", "campaign body as Markdown").option("--content-file <path>", "read the campaign body (Markdown) from a file").option("--from-name <name>", "From display name").option("--reply-to <email>", "Reply-To address (requires --from-name)").option(
12083
+ ).requiredOption("--name <name>", "internal label shown in the campaigns list (not the subject)").option("--subject <subject>", "email subject line").option("--preheader <text>", "preview text shown after the subject in inboxes").option("--content <markdown>", "campaign body as Markdown").option("--content-file <path>", "read the campaign body (Markdown) from a file").option(
12084
+ "--html-file <path>",
12085
+ "create a designed e-mail from a standalone HTML file (instead of a Markdown body)"
12086
+ ).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(
11836
12087
  "--from-email <email>",
11837
12088
  "custom From on the workspace's verified sending domain (requires --from-name); omit for the platform sender"
11838
12089
  ).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(
11839
12090
  async (options, command) => {
11840
12091
  const flags = getGlobalFlags(command);
11841
12092
  const contentMarkdown = resolveContent(options);
12093
+ const contentHtml = resolveHtml(options);
11842
12094
  const sender = resolveSender(options);
11843
12095
  const audience = resolveAudience(options);
11844
12096
  const theme = resolveTheme(options);
@@ -11848,6 +12100,8 @@ function registerCampaignsCommands(program2) {
11848
12100
  subject: options.subject,
11849
12101
  preheader: options.preheader,
11850
12102
  contentMarkdown,
12103
+ ...contentHtml !== void 0 ? { contentFormat: "html", contentHtml } : {},
12104
+ templateId: resolveTemplateId(options),
11851
12105
  sender,
11852
12106
  audience,
11853
12107
  theme
@@ -11860,7 +12114,7 @@ function registerCampaignsCommands(program2) {
11860
12114
  });
11861
12115
  }
11862
12116
  );
11863
- 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("--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(
12117
+ 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(
11864
12118
  async (id, options, command) => {
11865
12119
  const flags = getGlobalFlags(command);
11866
12120
  const input = {};
@@ -11869,6 +12123,10 @@ function registerCampaignsCommands(program2) {
11869
12123
  if (options.preheader !== void 0) input.preheader = options.preheader;
11870
12124
  const contentMarkdown = resolveContent(options);
11871
12125
  if (contentMarkdown !== void 0) input.contentMarkdown = contentMarkdown;
12126
+ const contentHtml = resolveHtml(options);
12127
+ if (contentHtml !== void 0) input.contentHtml = contentHtml;
12128
+ const templateId = resolveTemplateId(options);
12129
+ if (templateId !== void 0) input.templateId = templateId;
11872
12130
  const sender = resolveSender(options);
11873
12131
  if (sender !== void 0) input.sender = sender;
11874
12132
  const audience = resolveAudience(options);
@@ -11947,7 +12205,12 @@ function registerCampaignsCommands(program2) {
11947
12205
  );
11948
12206
  });
11949
12207
  });
11950
- campaigns.command("recipients").description("List a campaign's recipients (report drill-down), optionally by status").argument("<id>", "campaign id").option("--status <status>", `filter to one status (${RECIPIENT_STATUSES.join(" | ")})`).option("--limit <n>", "page size").option("--cursor <cursor>", "cursor from a previous page").action(
12208
+ campaigns.command("recipients").description(
12209
+ "List a campaign's recipients (report drill-down) with per-recipient opens/clicks, optionally by status or by clicked link"
12210
+ ).argument("<id>", "campaign id").option("--status <status>", `filter to one status (${RECIPIENT_STATUSES.join(" | ")})`).option(
12211
+ "--link <linkId>",
12212
+ "only recipients who clicked this link (id from `campaigns links`); wins over --status"
12213
+ ).option("--limit <n>", "page size").option("--cursor <cursor>", "cursor from a previous page").action(
11951
12214
  async (id, options, command) => {
11952
12215
  const flags = getGlobalFlags(command);
11953
12216
  let status;
@@ -11963,6 +12226,7 @@ function registerCampaignsCommands(program2) {
11963
12226
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11964
12227
  const page = await client.listCampaignRecipients(id, {
11965
12228
  status,
12229
+ linkId: options.link,
11966
12230
  limit: options.limit !== void 0 ? parsePositiveInt2(options.limit, "--limit") : void 0,
11967
12231
  cursor: options.cursor
11968
12232
  });
@@ -11972,10 +12236,13 @@ function registerCampaignsCommands(program2) {
11972
12236
  id: r.id,
11973
12237
  email: r.email,
11974
12238
  status: r.status,
12239
+ // "-" = not measured (opt-out or tracking off), distinct from 0.
12240
+ opens: r.tracked ? r.openCount : "-",
12241
+ clicks: r.tracked ? r.clickCount : "-",
11975
12242
  processedAt: r.processedAt ?? "",
11976
12243
  details: r.statusDetails ?? ""
11977
12244
  })),
11978
- ["id", "email", "status", "processedAt", "details"]
12245
+ ["id", "email", "status", "opens", "clicks", "processedAt", "details"]
11979
12246
  );
11980
12247
  if (page.nextCursor) {
11981
12248
  process.stdout.write(`
@@ -11985,6 +12252,30 @@ Next page: --cursor ${page.nextCursor}
11985
12252
  });
11986
12253
  }
11987
12254
  );
12255
+ campaigns.command("links").description("A sent campaign's link table: per-link unique/total clicks, most-clicked first").argument("<id>", "campaign id").action(async (id, _options, command) => {
12256
+ const flags = getGlobalFlags(command);
12257
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12258
+ const result = await client.listCampaignLinks(id);
12259
+ emit(flags, result, () => {
12260
+ if (result.links.length === 0) {
12261
+ process.stdout.write(
12262
+ "No clicks recorded yet (or the campaign was sent with tracking off).\n"
12263
+ );
12264
+ return;
12265
+ }
12266
+ printTable(
12267
+ result.links.map((link) => ({
12268
+ id: link.id,
12269
+ url: link.url,
12270
+ uniqueClicks: link.uniqueClicks,
12271
+ clicks: link.clicks,
12272
+ lastClickedAt: link.lastClickedAt
12273
+ })),
12274
+ ["id", "url", "uniqueClicks", "clicks", "lastClickedAt"]
12275
+ );
12276
+ process.stdout.write("\nWho clicked a link: campaigns recipients <id> --link <linkId>\n");
12277
+ });
12278
+ });
11988
12279
  }
11989
12280
 
11990
12281
  // src/commands/checkoutSettings/index.ts
@@ -12229,7 +12520,7 @@ function buildConsentInput(options) {
12229
12520
  }
12230
12521
 
12231
12522
  // src/commands/clients/index.ts
12232
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
12523
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
12233
12524
 
12234
12525
  // src/client/types.ts
12235
12526
  var AIImageAspectRatio = /* @__PURE__ */ ((AIImageAspectRatio2) => {
@@ -12283,66 +12574,505 @@ var PORTAL_LIBRARY_ORDERS = [
12283
12574
  "custom"
12284
12575
  ];
12285
12576
  var CUSTOMER_ACCESS_REPLACEMENT_STRATEGIES = ["adaptive", "always_allow", "creator_approval_only"];
12577
+ var CLIENT_TAG_COLORS = [
12578
+ "magenta",
12579
+ "red",
12580
+ "volcano",
12581
+ "orange",
12582
+ "gold",
12583
+ "lime",
12584
+ "green",
12585
+ "cyan",
12586
+ "blue",
12587
+ "geekblue",
12588
+ "purple"
12589
+ ];
12590
+ var TRANSCRIPTION_LANGUAGES = ["pl", "en", "default"];
12286
12591
 
12287
- // src/commands/clients/index.ts
12288
- var IMPORT_CHUNK_SIZE = 100;
12289
- var SCHEDULER_UNAVAILABLE_STATUS = 503;
12290
- var MAX_BULK_FILTERED_PRODUCTS = 10;
12291
- function requireConfirm3(confirm, what) {
12292
- if (!confirm) {
12293
- throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
12592
+ // src/commands/clients/emails.ts
12593
+ import { readFileSync as readFileSync6 } from "fs";
12594
+ function parsePositiveInt3(value, flag) {
12595
+ const n = Number.parseInt(value, 10);
12596
+ if (!Number.isInteger(n) || n <= 0) {
12597
+ throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
12294
12598
  }
12599
+ return n;
12295
12600
  }
12296
- function withSchedulerHint(error) {
12297
- if (error instanceof ApiError && error.status === SCHEDULER_UNAVAILABLE_STATUS) {
12601
+ function resolveContent2(options) {
12602
+ if (options.content !== void 0 && options.contentFile !== void 0) {
12603
+ throw new Error("Use either --content or --content-file, not both.");
12604
+ }
12605
+ return options.contentFile !== void 0 ? readFileSync6(options.contentFile, "utf8") : options.content;
12606
+ }
12607
+ function resolveSender2(options) {
12608
+ if (options.fromName === void 0 && options.replyTo === void 0 && options.fromEmail === void 0) {
12609
+ return void 0;
12610
+ }
12611
+ return {
12612
+ ...options.fromName !== void 0 ? { fromName: options.fromName } : {},
12613
+ ...options.replyTo !== void 0 ? { replyTo: options.replyTo } : {},
12614
+ ...options.fromEmail !== void 0 ? { fromEmail: options.fromEmail } : {}
12615
+ };
12616
+ }
12617
+ function resolveBranded(options) {
12618
+ if (options.branded && options.plain) {
12619
+ throw new Error("Use either --branded or --plain, not both.");
12620
+ }
12621
+ if (options.branded) return true;
12622
+ if (options.plain) return false;
12623
+ return void 0;
12624
+ }
12625
+ function printEmails(emails) {
12626
+ printTable(
12627
+ emails.map((e) => ({
12628
+ id: e.id,
12629
+ clientId: e.clientId,
12630
+ to: e.recipientEmail,
12631
+ status: e.status,
12632
+ subject: e.subject,
12633
+ sentAt: e.sentAt ?? "",
12634
+ updatedAt: e.updatedAt
12635
+ })),
12636
+ ["id", "clientId", "to", "status", "subject", "sentAt", "updatedAt"]
12637
+ );
12638
+ }
12639
+ function printEmailDetail(email) {
12640
+ process.stdout.write(`Client e-mail ${email.id} (${email.status})
12641
+ `);
12642
+ process.stdout.write(
12643
+ ` To: ${email.recipientName ? `${email.recipientName} ` : ""}<${email.recipientEmail}> (client ${email.clientId})
12644
+ `
12645
+ );
12646
+ process.stdout.write(
12647
+ ` From: ${email.sender.fromName}${email.sender.fromEmail ? ` <${email.sender.fromEmail}>` : " (platform sender)"}${email.sender.replyTo ? `, reply-to ${email.sender.replyTo}` : ""}
12648
+ `
12649
+ );
12650
+ process.stdout.write(` Subject: ${email.subject}
12651
+ `);
12652
+ process.stdout.write(` Frame: ${email.branded ? "branded" : "plain"}
12653
+ `);
12654
+ if (email.sentAt) process.stdout.write(` Sent at: ${email.sentAt}
12655
+ `);
12656
+ if (email.failureReason) process.stdout.write(` Failure: ${email.failureReason}
12657
+ `);
12658
+ }
12659
+ var SEND_WARNING = "delivers a REAL e-mail to the customer and cannot be recalled once it is out";
12660
+ var SEND_FAILED_STATUS = 502;
12661
+ function withSendFailedHint(error) {
12662
+ if (error instanceof ApiError && error.status === SEND_FAILED_STATUS) {
12298
12663
  throw new ApiError(
12299
12664
  error.status,
12300
- "Trigger.dev scheduler not configured \u2014 bulk client actions cannot be started on this environment.",
12301
- "The API needs TRIGGER_SECRET_KEY (Trigger.dev) set for the workspace services; ask the operator to configure it."
12665
+ error.apiMessage,
12666
+ "The send did not complete. Check the status with `clients emails list` \u2014 `failed` means nothing went out and it can be sent again. Re-running `clients emails send` is safe: the same draft is never delivered twice."
12302
12667
  );
12303
12668
  }
12304
12669
  throw error;
12305
12670
  }
12306
- function parseCommaList2(value, flag) {
12307
- const items = [
12671
+ function registerClientEmailsCommands(clients2) {
12672
+ const emails = clients2.command("emails").description(
12673
+ "One-off, personal e-mails from you to ONE customer (the customer card \u2192 E-mail). Drafts are harmless; `send` e-mails the customer for real"
12674
+ );
12675
+ emails.command("inbox").description("The workspace-wide feed: drafts + sent e-mails across every customer").option("--limit <n>", "max entries (default 200, max 500)").action(async (options, command) => {
12676
+ const flags = getGlobalFlags(command);
12677
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12678
+ const page = await client.listWorkspaceClientEmails(
12679
+ options.limit !== void 0 ? parsePositiveInt3(options.limit, "--limit") : void 0
12680
+ );
12681
+ emit(flags, page, () => printEmails(page.emails));
12682
+ });
12683
+ emails.command("list").description("One customer's e-mails (drafts + sent), newest first").argument("<clientId>", "workspace client id (wcid)").action(async (clientId, _options, command) => {
12684
+ const flags = getGlobalFlags(command);
12685
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12686
+ const page = await client.listClientEmails(clientId);
12687
+ emit(flags, page, () => printEmails(page.emails));
12688
+ });
12689
+ emails.command("draft").description(
12690
+ "Create a DRAFT e-mail to one customer \u2014 nothing is sent until `clients emails send`"
12691
+ ).argument("<clientId>", "workspace client id (wcid)").requiredOption("--subject <subject>", "subject line (max 200 chars)").option("--content <markdown>", "e-mail body as Markdown").option("--content-file <path>", "read the e-mail body (Markdown) from a file").option("--from-name <name>", "From display name (default: the workspace sender)").option("--reply-to <email>", "Reply-To address").option("--from-email <email>", "From address on the workspace's verified sending domain").option("--branded", "wrap it in the workspace's branded frame").option("--plain", "a plain, personal-looking e-mail (the default)").action(
12692
+ async (clientId, options, command) => {
12693
+ const flags = getGlobalFlags(command);
12694
+ const contentMarkdown = resolveContent2(options);
12695
+ if (contentMarkdown === void 0) {
12696
+ throw new Error("Pass the body with --content or --content-file.");
12697
+ }
12698
+ const sender = resolveSender2(options);
12699
+ const branded = resolveBranded(options);
12700
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12701
+ const result = await client.createClientEmailDraft(clientId, {
12702
+ subject: options.subject,
12703
+ contentMarkdown,
12704
+ ...sender !== void 0 ? { sender } : {},
12705
+ ...branded !== void 0 ? { branded } : {}
12706
+ });
12707
+ emit(flags, result, () => {
12708
+ printEmailDetail(result.email);
12709
+ process.stdout.write(
12710
+ `Nothing is sent yet \u2014 send it with \`clients emails send ${clientId} ${result.email.id} --confirm\`.
12711
+ `
12712
+ );
12713
+ });
12714
+ }
12715
+ );
12716
+ emails.command("update").description("Edit a DRAFT (a sent e-mail is a record and cannot be edited)").argument("<clientId>", "workspace client id (wcid)").argument("<emailId>", "e-mail id (from `clients emails list`)").option("--subject <subject>", "new subject line").option("--content <markdown>", "new body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new body (Markdown) from a file").option("--from-name <name>", "new From display name").option("--reply-to <email>", "new Reply-To address").option("--from-email <email>", "new From address on the verified sending domain").option("--branded", "wrap it in the workspace's branded frame").option("--plain", "a plain, personal-looking e-mail").action(
12717
+ async (clientId, emailId, options, command) => {
12718
+ const flags = getGlobalFlags(command);
12719
+ const input = {};
12720
+ if (options.subject !== void 0) input.subject = options.subject;
12721
+ const contentMarkdown = resolveContent2(options);
12722
+ if (contentMarkdown !== void 0) input.contentMarkdown = contentMarkdown;
12723
+ const sender = resolveSender2(options);
12724
+ if (sender !== void 0) input.sender = sender;
12725
+ const branded = resolveBranded(options);
12726
+ if (branded !== void 0) input.branded = branded;
12727
+ if (Object.keys(input).length === 0) {
12728
+ throw new Error(
12729
+ "Nothing to update \u2014 pass at least one change flag (see `clients emails update --help`)."
12730
+ );
12731
+ }
12732
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12733
+ const result = await client.updateClientEmailDraft(clientId, emailId, input);
12734
+ emit(flags, result, () => printEmailDetail(result.email));
12735
+ }
12736
+ );
12737
+ emails.command("delete").description("Delete a DRAFT (sent e-mails stay as a record). Requires --confirm").argument("<clientId>", "workspace client id (wcid)").argument("<emailId>", "e-mail id").option("--confirm", "required \u2014 acknowledge the deletion").action(
12738
+ async (clientId, emailId, options, command) => {
12739
+ const flags = getGlobalFlags(command);
12740
+ if (!options.confirm) {
12741
+ throw new Error(
12742
+ `Refusing to delete draft ${emailId} without --confirm. Re-run with --confirm to proceed.`
12743
+ );
12744
+ }
12745
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12746
+ const result = await client.deleteClientEmailDraft(clientId, emailId);
12747
+ emit(flags, result, () => {
12748
+ process.stdout.write(`Deleted draft ${emailId}.
12749
+ `);
12750
+ });
12751
+ }
12752
+ );
12753
+ emails.command("send").description(`\u26A0\uFE0F SENDS the draft \u2014 ${SEND_WARNING}. Requires --confirm`).argument("<clientId>", "workspace client id (wcid)").argument("<emailId>", "e-mail id (a draft, or a failed send to retry)").option("--confirm", "required \u2014 acknowledge this e-mails the customer for real").action(
12754
+ async (clientId, emailId, options, command) => {
12755
+ const flags = getGlobalFlags(command);
12756
+ if (!options.confirm) {
12757
+ throw new Error(
12758
+ `Refusing to send without --confirm. \`clients emails send\` ${SEND_WARNING}. Check the draft first with \`clients emails list ${clientId}\`, then re-run with --confirm.`
12759
+ );
12760
+ }
12761
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12762
+ const result = await client.sendClientEmail(clientId, emailId).catch(withSendFailedHint);
12763
+ emit(flags, result, () => {
12764
+ process.stdout.write(
12765
+ result.email.status === "sent" ? `Sent "${result.email.subject}" to ${result.email.recipientEmail}.
12766
+ ` : `E-mail ${result.email.id} is ${result.email.status}${result.email.failureReason ? ` \u2014 ${result.email.failureReason}` : ""}.
12767
+ `
12768
+ );
12769
+ });
12770
+ }
12771
+ );
12772
+ }
12773
+
12774
+ // src/commands/clients/subscription.ts
12775
+ var MAX_SUBSCRIPTION_CLIENTS_PER_REQUEST = 200;
12776
+ var MAX_REASON_NOTE_LENGTH = 500;
12777
+ var SUBSCRIPTION_STATUSES = ["subscribed", "unsubscribed"];
12778
+ var EMAIL_REASONS = [
12779
+ "imported-from-other-list",
12780
+ "existing-customers-consent",
12781
+ "asked-directly",
12782
+ "other"
12783
+ ];
12784
+ var SMS_REASONS = [
12785
+ "explicit-sms-consent",
12786
+ "asked-directly",
12787
+ "other"
12788
+ ];
12789
+ function parseIds(value) {
12790
+ const ids = [
12308
12791
  ...new Set(
12309
- value.split(",").map((item) => item.trim()).filter(Boolean)
12792
+ value.split(",").map((item) => item.trim()).filter((item) => item.length > 0)
12310
12793
  )
12311
12794
  ];
12312
- if (items.length === 0) {
12313
- throw new Error(`Invalid ${flag}: expected at least one value (comma-separated).`);
12314
- }
12315
- return items;
12795
+ if (ids.length === 0) throw new Error("--id needs at least one client id.");
12796
+ return ids;
12316
12797
  }
12317
- function resolveBulkSelection(options) {
12318
- if (options.email === void 0 === (options.all !== true)) {
12319
- throw new Error("Pass exactly one of --email <a,b,\u2026> or --all.");
12798
+ function parseStatus(value) {
12799
+ const match = SUBSCRIPTION_STATUSES.find((s) => s === value);
12800
+ if (!match) {
12801
+ throw new Error(
12802
+ `Invalid --status: ${value} (expected one of ${SUBSCRIPTION_STATUSES.join(", ")})`
12803
+ );
12320
12804
  }
12321
- if (options.all) return { processAllClients: true };
12322
- return { clientEmails: parseCommaList2(options.email ?? "", "--email"), processAllClients: false };
12323
- }
12324
- function describeBulkSelection(options) {
12325
- return options.all ? "ALL active clients" : `client(s) ${options.email ?? ""}`;
12805
+ return match;
12326
12806
  }
12327
- function parseExportColumns(value) {
12328
- return parseCommaList2(value, "--columns").map((column) => {
12329
- const match = CLIENTS_EXPORT_COLUMN_GROUPS.find((group) => group === column);
12330
- if (match === void 0) {
12331
- throw new Error(
12332
- `Invalid --columns entry: ${column} (expected ${CLIENTS_EXPORT_COLUMN_GROUPS.join(", ")})`
12333
- );
12807
+ function parseReason(options, status, reasons) {
12808
+ if (status === "unsubscribed") {
12809
+ if (options.reason !== void 0 || options.reasonNote !== void 0) {
12810
+ throw new Error("--reason / --reason-note only apply to --status subscribed.");
12334
12811
  }
12335
- return match;
12336
- });
12812
+ return void 0;
12813
+ }
12814
+ const match = reasons.find((r) => r === options.reason);
12815
+ if (!match) {
12816
+ throw new Error(
12817
+ `--status subscribed needs --reason (the consent basis you attest to): one of ${reasons.join(", ")}`
12818
+ );
12819
+ }
12820
+ if (options.reasonNote !== void 0 && options.reasonNote.length > MAX_REASON_NOTE_LENGTH) {
12821
+ throw new Error(`--reason-note is limited to ${MAX_REASON_NOTE_LENGTH} characters.`);
12822
+ }
12823
+ return match;
12337
12824
  }
12338
- function assertIsoDate(value, flag) {
12339
- if (Number.isNaN(Date.parse(value))) {
12340
- throw new Error(`Invalid ${flag}: ${value} (expected an ISO date, e.g. 2026-07-01)`);
12825
+ function requireConfirm3(options, what) {
12826
+ if (!options.confirm) {
12827
+ throw new Error(
12828
+ `Refusing to ${what} without --confirm \u2014 this changes marketing consent records. Re-run with --confirm to proceed.`
12829
+ );
12830
+ }
12831
+ }
12832
+ function chunk(ids) {
12833
+ const chunks = [];
12834
+ for (let offset = 0; offset < ids.length; offset += MAX_SUBSCRIPTION_CLIENTS_PER_REQUEST) {
12835
+ chunks.push(ids.slice(offset, offset + MAX_SUBSCRIPTION_CLIENTS_PER_REQUEST));
12836
+ }
12837
+ return chunks;
12838
+ }
12839
+ function mergeResults(pages) {
12840
+ return {
12841
+ updatedClientIds: pages.flatMap((p) => p.updatedClientIds),
12842
+ unchangedClientIds: pages.flatMap((p) => p.unchangedClientIds),
12843
+ skippedClientIds: pages.flatMap((p) => p.skippedClientIds)
12844
+ };
12845
+ }
12846
+ function printSummary(channel, status, summary) {
12847
+ process.stdout.write(
12848
+ `${channel}: ${summary.updatedClientIds.length} set to ${status}, ${summary.unchangedClientIds.length} already ${status}, ${summary.skippedClientIds.length} skipped (missing / merged / deleted).
12849
+ `
12850
+ );
12851
+ }
12852
+ function registerClientSubscriptionCommands(clients2) {
12853
+ clients2.command("email-subscription").description(
12854
+ `Subscribe / unsubscribe MANY clients to marketing e-mail at once. Subscribing records the consent basis YOU attest to (--reason). Sent in batches of ${MAX_SUBSCRIPTION_CLIENTS_PER_REQUEST}. Requires --confirm`
12855
+ ).requiredOption("--id <ids>", "comma-separated workspace client ids (wcid)").requiredOption("--status <status>", SUBSCRIPTION_STATUSES.join(" | ")).option("--reason <reason>", `consent basis when subscribing (${EMAIL_REASONS.join(" | ")})`).option("--reason-note <text>", "optional detail for --reason").option("--confirm", "required \u2014 acknowledge the consent change").action(async (options, command) => {
12856
+ const flags = getGlobalFlags(command);
12857
+ const clientIds = parseIds(options.id);
12858
+ const status = parseStatus(options.status);
12859
+ const reason = parseReason(options, status, EMAIL_REASONS);
12860
+ requireConfirm3(options, `set ${clientIds.length} client(s) to e-mail ${status}`);
12861
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12862
+ const pages = [];
12863
+ for (const ids of chunk(clientIds)) {
12864
+ pages.push(
12865
+ await client.bulkSetClientsEmailSubscription(
12866
+ reason !== void 0 ? { clientIds: ids, status: "subscribed", reason, reasonNote: options.reasonNote } : { clientIds: ids, status: "unsubscribed" }
12867
+ )
12868
+ );
12869
+ }
12870
+ const summary = mergeResults(pages);
12871
+ emit(flags, summary, () => printSummary("Marketing e-mail", status, summary));
12872
+ });
12873
+ clients2.command("sms-subscription").description(
12874
+ `Subscribe / unsubscribe MANY clients to marketing SMS at once. SMS is its own consent \u2014 an e-mail opt-in never covers it \u2014 so subscribing needs an SMS-specific --reason. Sent in batches of ${MAX_SUBSCRIPTION_CLIENTS_PER_REQUEST}. Requires --confirm`
12875
+ ).requiredOption("--id <ids>", "comma-separated workspace client ids (wcid)").requiredOption("--status <status>", SUBSCRIPTION_STATUSES.join(" | ")).option("--reason <reason>", `consent basis when subscribing (${SMS_REASONS.join(" | ")})`).option("--reason-note <text>", "optional detail for --reason").option("--confirm", "required \u2014 acknowledge the consent change").action(async (options, command) => {
12876
+ const flags = getGlobalFlags(command);
12877
+ const clientIds = parseIds(options.id);
12878
+ const status = parseStatus(options.status);
12879
+ const reason = parseReason(options, status, SMS_REASONS);
12880
+ requireConfirm3(options, `set ${clientIds.length} client(s) to SMS ${status}`);
12881
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12882
+ const pages = [];
12883
+ for (const ids of chunk(clientIds)) {
12884
+ pages.push(
12885
+ await client.bulkSetClientsSmsSubscription(
12886
+ reason !== void 0 ? { clientIds: ids, status: "subscribed", reason, reasonNote: options.reasonNote } : { clientIds: ids, status: "unsubscribed" }
12887
+ )
12888
+ );
12889
+ }
12890
+ const summary = mergeResults(pages);
12891
+ emit(flags, summary, () => printSummary("Marketing SMS", status, summary));
12892
+ });
12893
+ }
12894
+
12895
+ // src/commands/clients/tags.ts
12896
+ var MAX_TAG_CLIENTS_PER_REQUEST = 200;
12897
+ function parseCommaList2(value, flag) {
12898
+ const items = [
12899
+ ...new Set(
12900
+ value.split(",").map((item) => item.trim()).filter((item) => item.length > 0)
12901
+ )
12902
+ ];
12903
+ if (items.length === 0) throw new Error(`${flag} needs at least one value.`);
12904
+ return items;
12905
+ }
12906
+ function parseColor(value) {
12907
+ const match = CLIENT_TAG_COLORS.find((color) => color === value);
12908
+ if (!match) {
12909
+ throw new Error(`Invalid --color: ${value} (expected one of ${CLIENT_TAG_COLORS.join(", ")})`);
12910
+ }
12911
+ return match;
12912
+ }
12913
+ function resolveColor(options) {
12914
+ if (options.color !== void 0 && options.clearColor) {
12915
+ throw new Error("Use either --color or --clear-color, not both.");
12916
+ }
12917
+ if (options.clearColor) return null;
12918
+ return options.color !== void 0 ? parseColor(options.color) : void 0;
12919
+ }
12920
+ function registerClientTagsCommands(clients2) {
12921
+ const tags = clients2.command("tags").description(
12922
+ "Workspace client tags (Customers \u2192 Tags): the registry that segments, campaign link actions and imports refer to by id \u2014 list, create, rename / recolour, delete, assign"
12923
+ );
12924
+ tags.command("list", { isDefault: true }).description(
12925
+ "List the workspace client tags (id, name, color) \u2014 the ids `clients import --tag` accepts"
12926
+ ).action(async (_options, command) => {
12927
+ const flags = getGlobalFlags(command);
12928
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12929
+ const result = await client.listClientTags();
12930
+ emit(flags, result, () => {
12931
+ printTable(
12932
+ result.tags.map((tag) => ({ id: tag.id, name: tag.name, color: tag.color ?? "" })),
12933
+ ["id", "name", "color"]
12934
+ );
12935
+ });
12936
+ });
12937
+ tags.command("create").description("Create a client tag (names are unique per workspace)").requiredOption("--name <name>", "tag name").option("--color <color>", `tag colour preset (${CLIENT_TAG_COLORS.join(" | ")})`).action(async (options, command) => {
12938
+ const flags = getGlobalFlags(command);
12939
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12940
+ const result = await client.createClientTag({
12941
+ name: options.name,
12942
+ color: options.color !== void 0 ? parseColor(options.color) : void 0
12943
+ });
12944
+ emit(flags, result, () => {
12945
+ process.stdout.write(`Created tag ${result.tag.id} (${result.tag.name}).
12946
+ `);
12947
+ });
12948
+ });
12949
+ tags.command("update").description(
12950
+ "Rename and/or recolour a tag \u2014 the id stays the same, so every assignment, segment and campaign link action keeps pointing at it"
12951
+ ).argument("<tagId>", "tag id (from `clients tags list`)").option("--name <name>", "new tag name").option("--color <color>", `new tag colour preset (${CLIENT_TAG_COLORS.join(" | ")})`).option("--clear-color", "remove the colour").action(
12952
+ async (tagId, options, command) => {
12953
+ const flags = getGlobalFlags(command);
12954
+ const input = {};
12955
+ if (options.name !== void 0) input.name = options.name;
12956
+ const color = resolveColor(options);
12957
+ if (color !== void 0) input.color = color;
12958
+ if (Object.keys(input).length === 0) {
12959
+ throw new Error("Nothing to update \u2014 pass --name, --color or --clear-color.");
12960
+ }
12961
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12962
+ const result = await client.updateClientTag(tagId, input);
12963
+ emit(flags, result, () => {
12964
+ process.stdout.write(`Updated tag ${result.tag.id} (${result.tag.name}).
12965
+ `);
12966
+ });
12967
+ }
12968
+ );
12969
+ tags.command("delete").description(
12970
+ "PERMANENTLY delete a tag and remove it from every client that carries it \u2014 cannot be undone. Requires --confirm"
12971
+ ).argument("<tagId>", "tag id (from `clients tags list`)").option("--confirm", "required \u2014 acknowledge the permanent deletion").action(async (tagId, options, command) => {
12972
+ const flags = getGlobalFlags(command);
12973
+ if (!options.confirm) {
12974
+ throw new Error(
12975
+ `Refusing to delete tag ${tagId} without --confirm \u2014 it is removed from every client that carries it. Re-run with --confirm to proceed.`
12976
+ );
12977
+ }
12978
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12979
+ const result = await client.deleteClientTag(tagId);
12980
+ emit(flags, result, () => {
12981
+ process.stdout.write(
12982
+ `Deleted tag ${tagId} \u2014 removed from ${result.clientsUntagged} client${result.clientsUntagged === 1 ? "" : "s"}.
12983
+ `
12984
+ );
12985
+ });
12986
+ });
12987
+ tags.command("assign").description(
12988
+ `Add and/or remove tags on MANY clients at once (tag IDS, not names; unknown ids fail the request). Sent in batches of ${MAX_TAG_CLIENTS_PER_REQUEST}`
12989
+ ).requiredOption("--id <ids>", "comma-separated workspace client ids (wcid)").option("--add <tagIds>", "comma-separated tag ids to add").option("--remove <tagIds>", "comma-separated tag ids to remove").action(async (options, command) => {
12990
+ const flags = getGlobalFlags(command);
12991
+ const clientIds = parseCommaList2(options.id, "--id");
12992
+ const addTagIds = options.add !== void 0 ? parseCommaList2(options.add, "--add") : [];
12993
+ const removeTagIds = options.remove !== void 0 ? parseCommaList2(options.remove, "--remove") : [];
12994
+ if (addTagIds.length === 0 && removeTagIds.length === 0) {
12995
+ throw new Error("Pass --add and/or --remove with at least one tag id.");
12996
+ }
12997
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12998
+ const summary = { results: [], skippedClientIds: [] };
12999
+ for (let offset = 0; offset < clientIds.length; offset += MAX_TAG_CLIENTS_PER_REQUEST) {
13000
+ const page = await client.bulkUpdateClientTagAssignments({
13001
+ clientIds: clientIds.slice(offset, offset + MAX_TAG_CLIENTS_PER_REQUEST),
13002
+ ...addTagIds.length > 0 ? { addTagIds } : {},
13003
+ ...removeTagIds.length > 0 ? { removeTagIds } : {}
13004
+ });
13005
+ summary.results.push(...page.results);
13006
+ summary.skippedClientIds.push(...page.skippedClientIds);
13007
+ }
13008
+ emit(flags, summary, () => {
13009
+ process.stdout.write(
13010
+ `Updated tags on ${summary.results.length} client${summary.results.length === 1 ? "" : "s"}${summary.skippedClientIds.length > 0 ? `, skipped ${summary.skippedClientIds.length} (not found / merged)` : ""}.
13011
+ `
13012
+ );
13013
+ });
13014
+ });
13015
+ }
13016
+
13017
+ // src/commands/clients/index.ts
13018
+ var IMPORT_CHUNK_SIZE = 100;
13019
+ var SCHEDULER_UNAVAILABLE_STATUS = 503;
13020
+ var MAX_BULK_FILTERED_PRODUCTS = 10;
13021
+ function requireConfirm4(confirm, what) {
13022
+ if (!confirm) {
13023
+ throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
13024
+ }
13025
+ }
13026
+ function withSchedulerHint(error) {
13027
+ if (error instanceof ApiError && error.status === SCHEDULER_UNAVAILABLE_STATUS) {
13028
+ throw new ApiError(
13029
+ error.status,
13030
+ "Trigger.dev scheduler not configured \u2014 bulk client actions cannot be started on this environment.",
13031
+ "The API needs TRIGGER_SECRET_KEY (Trigger.dev) set for the workspace services; ask the operator to configure it."
13032
+ );
13033
+ }
13034
+ throw error;
13035
+ }
13036
+ function parseCommaList3(value, flag) {
13037
+ const items = [
13038
+ ...new Set(
13039
+ value.split(",").map((item) => item.trim()).filter(Boolean)
13040
+ )
13041
+ ];
13042
+ if (items.length === 0) {
13043
+ throw new Error(`Invalid ${flag}: expected at least one value (comma-separated).`);
13044
+ }
13045
+ return items;
13046
+ }
13047
+ function resolveBulkSelection(options) {
13048
+ if (options.email === void 0 === (options.all !== true)) {
13049
+ throw new Error("Pass exactly one of --email <a,b,\u2026> or --all.");
13050
+ }
13051
+ if (options.all) return { processAllClients: true };
13052
+ return { clientEmails: parseCommaList3(options.email ?? "", "--email"), processAllClients: false };
13053
+ }
13054
+ function describeBulkSelection(options) {
13055
+ return options.all ? "ALL active clients" : `client(s) ${options.email ?? ""}`;
13056
+ }
13057
+ function parseExportColumns(value) {
13058
+ return parseCommaList3(value, "--columns").map((column) => {
13059
+ const match = CLIENTS_EXPORT_COLUMN_GROUPS.find((group) => group === column);
13060
+ if (match === void 0) {
13061
+ throw new Error(
13062
+ `Invalid --columns entry: ${column} (expected ${CLIENTS_EXPORT_COLUMN_GROUPS.join(", ")})`
13063
+ );
13064
+ }
13065
+ return match;
13066
+ });
13067
+ }
13068
+ function assertIsoDate(value, flag) {
13069
+ if (Number.isNaN(Date.parse(value))) {
13070
+ throw new Error(`Invalid ${flag}: ${value} (expected an ISO date, e.g. 2026-07-01)`);
12341
13071
  }
12342
13072
  }
12343
13073
  function parseClientsFilter(value) {
12344
13074
  const trimmed = value.trim();
12345
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync6(value, "utf8");
13075
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync7(value, "utf8");
12346
13076
  let parsed;
12347
13077
  try {
12348
13078
  parsed = JSON.parse(raw);
@@ -12386,7 +13116,7 @@ function oneLine(text, max = 80) {
12386
13116
  const flat = text.replace(/\s+/g, " ").trim();
12387
13117
  return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
12388
13118
  }
12389
- function parsePositiveInt3(value, flag) {
13119
+ function parsePositiveInt4(value, flag) {
12390
13120
  const n = Number.parseInt(value, 10);
12391
13121
  if (!Number.isInteger(n) || n <= 0) {
12392
13122
  throw new Error(`Invalid ${flag}: ${value}`);
@@ -12465,7 +13195,7 @@ function resolveImportColumn(header) {
12465
13195
  }
12466
13196
  }
12467
13197
  function readImportCsv(csvFile) {
12468
- const rows = parseCsv(readFileSync6(csvFile, "utf8"));
13198
+ const rows = parseCsv(readFileSync7(csvFile, "utf8"));
12469
13199
  if (rows.length === 0) {
12470
13200
  throw new Error(`${csvFile} is empty.`);
12471
13201
  }
@@ -12561,7 +13291,7 @@ function registerClientsCommands(program2) {
12561
13291
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12562
13292
  const page = await client.listClients({
12563
13293
  search: options.search,
12564
- limit: options.limit !== void 0 ? parsePositiveInt3(options.limit, "--limit") : void 0,
13294
+ limit: options.limit !== void 0 ? parsePositiveInt4(options.limit, "--limit") : void 0,
12565
13295
  cursor: options.cursor,
12566
13296
  products: options.withProducts ? "has" : options.withoutProducts ? "no" : void 0
12567
13297
  });
@@ -12633,25 +13363,13 @@ Next page: --cursor ${page.nextCursor}
12633
13363
  }
12634
13364
  });
12635
13365
  });
12636
- clients2.command("tags").description(
12637
- "List the workspace client tags (id, name, color) \u2014 the ids `clients import --tag` accepts"
12638
- ).action(async (_options, command) => {
12639
- const flags = getGlobalFlags(command);
12640
- const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12641
- const result = await client.listClientTags();
12642
- emit(flags, result, () => {
12643
- printTable(
12644
- result.tags.map((tag) => ({ id: tag.id, name: tag.name, color: tag.color ?? "" })),
12645
- ["id", "name", "color"]
12646
- );
12647
- });
12648
- });
13366
+ registerClientTagsCommands(clients2);
12649
13367
  clients2.command("email-log").description("Show the emails Zanfia sent to a client and their delivery status (newest first)").argument("<clientId>", "workspace client id (wcid)").option("--limit <n>", "max entries (default 20, max 50)").action(async (clientId, options, command) => {
12650
13368
  const flags = getGlobalFlags(command);
12651
13369
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12652
13370
  const result = await client.getClientEmailLog(
12653
13371
  clientId,
12654
- options.limit !== void 0 ? parsePositiveInt3(options.limit, "--limit") : void 0
13372
+ options.limit !== void 0 ? parsePositiveInt4(options.limit, "--limit") : void 0
12655
13373
  );
12656
13374
  emit(flags, result, () => {
12657
13375
  process.stdout.write(`Email log for ${result.email}
@@ -12669,6 +13387,26 @@ Next page: --cursor ${page.nextCursor}
12669
13387
  );
12670
13388
  });
12671
13389
  });
13390
+ clients2.command("bundled-access").description(
13391
+ "Clients whose access to a product comes from a PARENT product's plan (bundle), not a direct grant \u2014 together with the direct grants this is the full access census of the product"
13392
+ ).argument("<productId>", "the INCLUDED product id").action(async (productId, _options, command) => {
13393
+ const flags = getGlobalFlags(command);
13394
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
13395
+ const result = await client.listBundledAccessClients(productId);
13396
+ emit(flags, result, () => {
13397
+ printTable(
13398
+ result.items.map((item) => ({
13399
+ clientId: item.clientId,
13400
+ email: item.email,
13401
+ name: [item.firstName, item.lastName].filter(Boolean).join(" "),
13402
+ via: item.viaProductName,
13403
+ plan: item.viaPlanName,
13404
+ since: item.sinceAt ?? ""
13405
+ })),
13406
+ ["clientId", "email", "name", "via", "plan", "since"]
13407
+ );
13408
+ });
13409
+ });
12672
13410
  clients2.command("accesses").description("List a client's product-access grants (active + archived), newest first").argument("<clientId>", "workspace client id (wcid)").option("--status <status>", `only grants in this state (${ACCESS_STATUSES.join(" | ")})`).action(async (clientId, options, command) => {
12673
13411
  const flags = getGlobalFlags(command);
12674
13412
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -12881,14 +13619,14 @@ Next page: --cursor ${page.nextCursor}
12881
13619
  let existing = 0;
12882
13620
  let invalid = 0;
12883
13621
  for (let offset = 0; offset < contacts.length; offset += IMPORT_CHUNK_SIZE) {
12884
- const chunk = contacts.slice(offset, offset + IMPORT_CHUNK_SIZE);
13622
+ const chunk2 = contacts.slice(offset, offset + IMPORT_CHUNK_SIZE);
12885
13623
  if (!flags.json && contacts.length > IMPORT_CHUNK_SIZE) {
12886
13624
  process.stderr.write(
12887
- `Importing rows ${offset + 1}\u2013${offset + chunk.length} of ${contacts.length}\u2026
13625
+ `Importing rows ${offset + 1}\u2013${offset + chunk2.length} of ${contacts.length}\u2026
12888
13626
  `
12889
13627
  );
12890
13628
  }
12891
- const page = await client.importClients({ contacts: chunk, tagIds });
13629
+ const page = await client.importClients({ contacts: chunk2, tagIds });
12892
13630
  results.push(...page.results);
12893
13631
  created += page.created;
12894
13632
  existing += page.existing;
@@ -12946,7 +13684,7 @@ Next page: --cursor ${page.nextCursor}
12946
13684
  async (productId, options, command) => {
12947
13685
  const flags = getGlobalFlags(command);
12948
13686
  const selection = resolveBulkSelection(options);
12949
- requireConfirm3(
13687
+ requireConfirm4(
12950
13688
  options.confirm,
12951
13689
  `revoke ${describeBulkSelection(options)} on product ${productId} (no refunds)`
12952
13690
  );
@@ -12983,7 +13721,7 @@ Next page: --cursor ${page.nextCursor}
12983
13721
  }
12984
13722
  addDaysToCurrentEndAccessPeriod = days;
12985
13723
  }
12986
- requireConfirm3(
13724
+ requireConfirm4(
12987
13725
  options.confirm,
12988
13726
  `change the access period of ${describeBulkSelection(options)} on product ${productId}`
12989
13727
  );
@@ -13004,7 +13742,7 @@ Next page: --cursor ${page.nextCursor}
13004
13742
  async (productId, options, command) => {
13005
13743
  const flags = getGlobalFlags(command);
13006
13744
  const selection = resolveBulkSelection(options);
13007
- requireConfirm3(
13745
+ requireConfirm4(
13008
13746
  options.confirm,
13009
13747
  `delete the grants of ${describeBulkSelection(options)} on product ${productId}`
13010
13748
  );
@@ -13027,14 +13765,14 @@ Next page: --cursor ${page.nextCursor}
13027
13765
  ).option("--run-tasks", "also run each product's access-end automation tasks per client").option("--confirm", "required \u2014 acknowledge the filter-wide revoke").action(
13028
13766
  async (options, command) => {
13029
13767
  const flags = getGlobalFlags(command);
13030
- const productIds = parseCommaList2(options.product, "--product");
13768
+ const productIds = parseCommaList3(options.product, "--product");
13031
13769
  if (productIds.length > MAX_BULK_FILTERED_PRODUCTS) {
13032
13770
  throw new Error(
13033
13771
  `--product accepts at most ${MAX_BULK_FILTERED_PRODUCTS} ids (got ${productIds.length}).`
13034
13772
  );
13035
13773
  }
13036
13774
  const clientsFilter = parseClientsFilter(options.filter);
13037
- requireConfirm3(
13775
+ requireConfirm4(
13038
13776
  options.confirm,
13039
13777
  `revoke every client matching the filter in ${productIds.length} product(s)`
13040
13778
  );
@@ -13054,14 +13792,14 @@ Next page: --cursor ${page.nextCursor}
13054
13792
  `\u26A0\uFE0F Delete (tombstone) clients by id \u2014 clients that still hold active product access are SKIPPED (revoke first). Sent in batches of ${MAX_DELETE_CLIENTS_PER_REQUEST}. Requires --confirm`
13055
13793
  ).requiredOption("--id <ids>", "comma-separated workspace client ids (wcid)").option("--confirm", "required \u2014 acknowledge the deletion").action(async (options, command) => {
13056
13794
  const flags = getGlobalFlags(command);
13057
- const clientIds = parseCommaList2(options.id, "--id");
13058
- requireConfirm3(options.confirm, `delete ${clientIds.length} client(s)`);
13795
+ const clientIds = parseCommaList3(options.id, "--id");
13796
+ requireConfirm4(options.confirm, `delete ${clientIds.length} client(s)`);
13059
13797
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
13060
13798
  const deletedClientIds = [];
13061
13799
  const skipped = [];
13062
13800
  for (let offset = 0; offset < clientIds.length; offset += MAX_DELETE_CLIENTS_PER_REQUEST) {
13063
- const chunk = clientIds.slice(offset, offset + MAX_DELETE_CLIENTS_PER_REQUEST);
13064
- const page = await client.deleteClients({ clientIds: chunk });
13801
+ const chunk2 = clientIds.slice(offset, offset + MAX_DELETE_CLIENTS_PER_REQUEST);
13802
+ const page = await client.deleteClients({ clientIds: chunk2 });
13065
13803
  deletedClientIds.push(...page.deletedClientIds);
13066
13804
  skipped.push(...page.skipped);
13067
13805
  }
@@ -13102,7 +13840,7 @@ Next page: --cursor ${page.nextCursor}
13102
13840
  columns: options.columns !== void 0 ? parseExportColumns(options.columns) : void 0,
13103
13841
  createdFrom: options.createdFrom,
13104
13842
  createdTo: options.createdTo,
13105
- limit: options.limit !== void 0 ? parsePositiveInt3(options.limit, "--limit") : void 0
13843
+ limit: options.limit !== void 0 ? parsePositiveInt4(options.limit, "--limit") : void 0
13106
13844
  });
13107
13845
  writeFileSync2(options.out, result.csv, "utf8");
13108
13846
  emit(
@@ -13149,7 +13887,7 @@ Next page: --cursor ${page.nextCursor}
13149
13887
  ).argument("<clientId>", "client id").argument("<deviceId>", "device id from `clients devices <clientId>`").option("--confirm", "required \u2014 acknowledge the device sign-out").action(
13150
13888
  async (clientId, deviceId, options, command) => {
13151
13889
  const flags = getGlobalFlags(command);
13152
- requireConfirm3(options.confirm, `revoke device ${deviceId} of client ${clientId}`);
13890
+ requireConfirm4(options.confirm, `revoke device ${deviceId} of client ${clientId}`);
13153
13891
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
13154
13892
  const result = await client.revokeClientDevice(clientId, deviceId);
13155
13893
  emit(flags, result, () => {
@@ -13165,7 +13903,7 @@ Next page: --cursor ${page.nextCursor}
13165
13903
  "Revoke EVERY device slot of the client (all devices signed out, limit counters cleared). Requires --confirm"
13166
13904
  ).argument("<clientId>", "client id").option("--confirm", "required \u2014 acknowledge signing out every device").action(async (clientId, options, command) => {
13167
13905
  const flags = getGlobalFlags(command);
13168
- requireConfirm3(options.confirm, `reset every device of client ${clientId}`);
13906
+ requireConfirm4(options.confirm, `reset every device of client ${clientId}`);
13169
13907
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
13170
13908
  const result = await client.resetClientDevices(clientId);
13171
13909
  emit(flags, result, () => {
@@ -13173,14 +13911,16 @@ Next page: --cursor ${page.nextCursor}
13173
13911
  `);
13174
13912
  });
13175
13913
  });
13914
+ registerClientSubscriptionCommands(clients2);
13915
+ registerClientEmailsCommands(clients2);
13176
13916
  }
13177
13917
 
13178
13918
  // src/commands/communities/index.ts
13179
- import { readFileSync as readFileSync7 } from "fs";
13919
+ import { readFileSync as readFileSync8 } from "fs";
13180
13920
  function clientFor(flags) {
13181
13921
  return createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
13182
13922
  }
13183
- function requireConfirm4(confirm, what) {
13923
+ function requireConfirm5(confirm, what) {
13184
13924
  if (!confirm) {
13185
13925
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
13186
13926
  }
@@ -13192,7 +13932,7 @@ function parseOnOff(value, flag) {
13192
13932
  }
13193
13933
  return normalized === "on";
13194
13934
  }
13195
- function parsePositiveInt4(value, flag) {
13935
+ function parsePositiveInt5(value, flag) {
13196
13936
  const n = Number.parseInt(value, 10);
13197
13937
  if (!Number.isInteger(n) || n <= 0) {
13198
13938
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer).`);
@@ -13282,11 +14022,11 @@ function authorLabel(author) {
13282
14022
  const name = [author.firstName, author.lastName].filter(Boolean).join(" ");
13283
14023
  return author.nickname || name || author.id;
13284
14024
  }
13285
- function resolveContent2(options) {
14025
+ function resolveContent3(options) {
13286
14026
  if (options.content !== void 0 && options.contentFile !== void 0) {
13287
14027
  throw new Error("Use either --content or --content-file, not both.");
13288
14028
  }
13289
- return options.contentFile !== void 0 ? readFileSync7(options.contentFile, "utf8") : options.content;
14029
+ return options.contentFile !== void 0 ? readFileSync8(options.contentFile, "utf8") : options.content;
13290
14030
  }
13291
14031
  function parsePage(options) {
13292
14032
  return {
@@ -13430,7 +14170,7 @@ Read a thread: --thread <messageId>
13430
14170
  chat.command("send").description("Send a message to a chat channel (use --reply-to for a thread reply)").argument("<communityId>", "community id").argument("<channelId>", "chat channel id").option("--content <text>", "message body").option("--content-file <path>", "read the message body from a file").option("--reply-to <messageId>", "reply in this message's thread").action(
13431
14171
  async (communityId, channelId, options, command) => {
13432
14172
  const flags = getGlobalFlags(command);
13433
- const content = resolveContent2(options);
14173
+ const content = resolveContent3(options);
13434
14174
  if (content === void 0 || content.trim().length === 0) {
13435
14175
  throw new Error("Pass the message body via --content or --content-file.");
13436
14176
  }
@@ -13450,7 +14190,7 @@ Read a thread: --thread <messageId>
13450
14190
  chat.command("edit").description("Edit an own chat message (pass --thread when it is a thread reply)").argument("<communityId>", "community id").argument("<channelId>", "chat channel id").argument("<messageId>", "message id").option("--content <text>", "new message body").option("--content-file <path>", "read the new message body from a file").option("--thread <parentMessageId>", "the message is a reply in this thread").action(
13451
14191
  async (communityId, channelId, messageId, options, command) => {
13452
14192
  const flags = getGlobalFlags(command);
13453
- const content = resolveContent2(options);
14193
+ const content = resolveContent3(options);
13454
14194
  if (content === void 0 || content.trim().length === 0) {
13455
14195
  throw new Error("Pass the new message body via --content or --content-file.");
13456
14196
  }
@@ -13670,7 +14410,7 @@ Read a thread: --thread <messageId>
13670
14410
  post.command("create").description("Create a post in a feed channel").argument("<communityId>", "community id").argument("<channelId>", "channel id").option("--title <title>", "post title").option("--content <markdown>", "post body (Markdown)").option("--content-file <path>", "read the post body (Markdown) from a file").action(
13671
14411
  async (communityId, channelId, options, command) => {
13672
14412
  const flags = getGlobalFlags(command);
13673
- const content = resolveContent2(options);
14413
+ const content = resolveContent3(options);
13674
14414
  if (content === void 0 || content.trim().length === 0) {
13675
14415
  throw new Error("Pass the post body via --content or --content-file.");
13676
14416
  }
@@ -13687,7 +14427,7 @@ Read a thread: --thread <messageId>
13687
14427
  post.command("update").description("Update a post (title and/or Markdown content)").argument("<communityId>", "community id").argument("<postId>", "post id").option("--title <title>", "new post title").option("--content <markdown>", "new post body (Markdown)").option("--content-file <path>", "read the new post body (Markdown) from a file").action(
13688
14428
  async (communityId, postId, options, command) => {
13689
14429
  const flags = getGlobalFlags(command);
13690
- const content = resolveContent2(options);
14430
+ const content = resolveContent3(options);
13691
14431
  if (options.title === void 0 && content === void 0) {
13692
14432
  throw new Error("Nothing to update \u2014 pass --title and/or --content/--content-file.");
13693
14433
  }
@@ -13756,7 +14496,7 @@ Next page: --cursor ${result.nextCursor}
13756
14496
  comment.command("create").description("Comment on a post (use --reply-to for a threaded reply)").argument("<communityId>", "community id").argument("<postId>", "post id").option("--content <markdown>", "comment body (Markdown)").option("--content-file <path>", "read the comment body (Markdown) from a file").option("--reply-to <commentId>", "parent comment id for a threaded reply").action(
13757
14497
  async (communityId, postId, options, command) => {
13758
14498
  const flags = getGlobalFlags(command);
13759
- const content = resolveContent2(options);
14499
+ const content = resolveContent3(options);
13760
14500
  if (content === void 0 || content.trim().length === 0) {
13761
14501
  throw new Error("Pass the comment body via --content or --content-file.");
13762
14502
  }
@@ -13773,7 +14513,7 @@ Next page: --cursor ${result.nextCursor}
13773
14513
  comment.command("update").description("Update a comment (Markdown content)").argument("<communityId>", "community id").argument("<commentId>", "comment id").option("--content <markdown>", "new comment body (Markdown)").option("--content-file <path>", "read the new comment body (Markdown) from a file").action(
13774
14514
  async (communityId, commentId, options, command) => {
13775
14515
  const flags = getGlobalFlags(command);
13776
- const content = resolveContent2(options);
14516
+ const content = resolveContent3(options);
13777
14517
  if (content === void 0 || content.trim().length === 0) {
13778
14518
  throw new Error("Pass the new comment body via --content or --content-file.");
13779
14519
  }
@@ -13861,7 +14601,7 @@ ${message.content}
13861
14601
  support.command("create").description("Raise a support ticket in a community (needs a support channel)").argument("<communityId>", "community id").requiredOption("--channel <channelId>", "the support channel id").requiredOption("--subject <subject>", "ticket subject").option("--message <markdown>", "ticket body (Markdown)").option("--message-file <path>", "read the ticket body (Markdown) from a file").action(
13862
14602
  async (communityId, options, command) => {
13863
14603
  const flags = getGlobalFlags(command);
13864
- const message = resolveContent2({
14604
+ const message = resolveContent3({
13865
14605
  content: options.message,
13866
14606
  contentFile: options.messageFile
13867
14607
  });
@@ -13882,7 +14622,7 @@ ${message.content}
13882
14622
  support.command("reply").description("Reply on a support ticket").argument("<communityId>", "community id").argument("<ticketId>", "ticket id").option("--content <markdown>", "reply body (Markdown)").option("--content-file <path>", "read the reply body (Markdown) from a file").action(
13883
14623
  async (communityId, ticketId, options, command) => {
13884
14624
  const flags = getGlobalFlags(command);
13885
- const content = resolveContent2(options);
14625
+ const content = resolveContent3(options);
13886
14626
  if (content === void 0 || content.trim().length === 0) {
13887
14627
  throw new Error("Pass the reply body via --content or --content-file.");
13888
14628
  }
@@ -14056,7 +14796,7 @@ Next page: --cursor ${result.nextCursor}
14056
14796
  const raw = options.autoHideAfterReports.trim().toLowerCase();
14057
14797
  patch.moderation = {
14058
14798
  ...patch.moderation ?? current.moderation ?? {},
14059
- autoHideAfterReports: raw === "off" ? null : parsePositiveInt4(options.autoHideAfterReports, "--auto-hide-after-reports")
14799
+ autoHideAfterReports: raw === "off" ? null : parsePositiveInt5(options.autoHideAfterReports, "--auto-hide-after-reports")
14060
14800
  };
14061
14801
  }
14062
14802
  const keys = Object.keys(patch);
@@ -14194,7 +14934,7 @@ Next page: --cursor ${result.nextCursor}
14194
14934
  from: options.from !== void 0 ? assertIsoInstant(options.from, "--from") : void 0,
14195
14935
  to: options.to !== void 0 ? assertIsoInstant(options.to, "--to") : void 0,
14196
14936
  excludeCancelled: options.excludeCancelled,
14197
- limit: options.limit !== void 0 ? parsePositiveInt4(options.limit, "--limit") : void 0
14937
+ limit: options.limit !== void 0 ? parsePositiveInt5(options.limit, "--limit") : void 0
14198
14938
  });
14199
14939
  emit(flags, result, () => {
14200
14940
  printTable(result.events.map(eventRow), EVENT_COLUMNS);
@@ -14282,7 +15022,7 @@ Next page: --cursor ${result.nextCursor}
14282
15022
  events.command("delete").description("Delete a calendar event (staff) \u2014 cannot be undone. Requires --confirm.").argument("<communityId>", "community id").argument("<eventId>", "event id").option("--confirm", "required \u2014 acknowledge the deletion").action(
14283
15023
  async (communityId, eventId, options, command) => {
14284
15024
  const flags = getGlobalFlags(command);
14285
- requireConfirm4(options.confirm, `delete event ${eventId}`);
15025
+ requireConfirm5(options.confirm, `delete event ${eventId}`);
14286
15026
  await clientFor(flags).deleteCommunityEvent(communityId, eventId);
14287
15027
  emit(flags, { communityId, eventId, deleted: true }, () => {
14288
15028
  process.stdout.write(`Deleted event ${eventId} from community ${communityId}.
@@ -14316,7 +15056,7 @@ Recently resolved (${result.resolved.length}):
14316
15056
  COMMUNITY_MODERATION_RESOLVE_ACTIONS,
14317
15057
  "--action"
14318
15058
  );
14319
- requireConfirm4(options.confirm, `${action} the content behind report ${reportId}`);
15059
+ requireConfirm5(options.confirm, `${action} the content behind report ${reportId}`);
14320
15060
  await clientFor(flags).resolveCommunityModerationReport(communityId, reportId, {
14321
15061
  action,
14322
15062
  ...options.note !== void 0 ? { note: options.note } : {}
@@ -14363,7 +15103,7 @@ function parseSettingsPatch(value) {
14363
15103
  }
14364
15104
  function parseJsonObject(value, flag) {
14365
15105
  const trimmed = value.trim();
14366
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync7(value, "utf8");
15106
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync8(value, "utf8");
14367
15107
  let parsed;
14368
15108
  try {
14369
15109
  parsed = JSON.parse(raw);
@@ -14377,20 +15117,20 @@ function parseJsonObject(value, flag) {
14377
15117
  }
14378
15118
 
14379
15119
  // src/commands/conversations/index.ts
14380
- import { readFileSync as readFileSync8 } from "fs";
15120
+ import { readFileSync as readFileSync9 } from "fs";
14381
15121
  var MAX_BROADCAST_RECIPIENTS = 5e3;
14382
- function parsePositiveInt5(value, flag) {
15122
+ function parsePositiveInt6(value, flag) {
14383
15123
  const n = Number.parseInt(value, 10);
14384
15124
  if (!Number.isInteger(n) || n <= 0) {
14385
15125
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
14386
15126
  }
14387
15127
  return n;
14388
15128
  }
14389
- function resolveContent3(options) {
15129
+ function resolveContent4(options) {
14390
15130
  if (options.content !== void 0 && options.contentFile !== void 0) {
14391
15131
  throw new Error("Use either --content or --content-file, not both.");
14392
15132
  }
14393
- const content = options.contentFile !== void 0 ? readFileSync8(options.contentFile, "utf8") : options.content;
15133
+ const content = options.contentFile !== void 0 ? readFileSync9(options.contentFile, "utf8") : options.content;
14394
15134
  if (content === void 0 || content.trim().length === 0) {
14395
15135
  throw new Error("Pass the message body via --content or --content-file.");
14396
15136
  }
@@ -14425,7 +15165,7 @@ function registerConversationsCommands(program2) {
14425
15165
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
14426
15166
  const page = await client.listConversations({
14427
15167
  unreadOnly: options.unread,
14428
- limit: options.limit !== void 0 ? parsePositiveInt5(options.limit, "--limit") : void 0,
15168
+ limit: options.limit !== void 0 ? parsePositiveInt6(options.limit, "--limit") : void 0,
14429
15169
  cursor: options.cursor
14430
15170
  });
14431
15171
  emit(flags, page, () => {
@@ -14475,7 +15215,7 @@ Next page: --cursor ${page.nextCursor}
14475
15215
  const flags = getGlobalFlags(command);
14476
15216
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
14477
15217
  const page = await client.listConversationMessages(clientId, {
14478
- limit: options.limit !== void 0 ? parsePositiveInt5(options.limit, "--limit") : void 0,
15218
+ limit: options.limit !== void 0 ? parsePositiveInt6(options.limit, "--limit") : void 0,
14479
15219
  cursor: options.cursor
14480
15220
  });
14481
15221
  emit(flags, page, () => {
@@ -14500,7 +15240,7 @@ Next page: --cursor ${page.nextCursor}
14500
15240
  conversations.command("send").description("Send a staff message into a client's conversation").argument("<clientId>", "client id (wcid)").option("--content <text>", "message body").option("--content-file <path>", "read the message body from a file").option("--message-id <id>", "idempotency id \u2014 resending with the same id is a no-op").action(
14501
15241
  async (clientId, options, command) => {
14502
15242
  const flags = getGlobalFlags(command);
14503
- const content = resolveContent3(options);
15243
+ const content = resolveContent4(options);
14504
15244
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
14505
15245
  const result = await client.sendConversationMessage(clientId, {
14506
15246
  content,
@@ -14515,7 +15255,7 @@ Next page: --cursor ${page.nextCursor}
14515
15255
  conversations.command("edit").description("Edit an own message (author-only)").argument("<clientId>", "client id (wcid)").argument("<messageId>", "message id").option("--content <text>", "new message body").option("--content-file <path>", "read the new message body from a file").action(
14516
15256
  async (clientId, messageId, options, command) => {
14517
15257
  const flags = getGlobalFlags(command);
14518
- const content = resolveContent3(options);
15258
+ const content = resolveContent4(options);
14519
15259
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
14520
15260
  const result = await client.editConversationMessage(clientId, messageId, content);
14521
15261
  emit(flags, result, () => {
@@ -14575,7 +15315,7 @@ Next page: --cursor ${page.nextCursor}
14575
15315
  const flags = getGlobalFlags(command);
14576
15316
  const recipients = [
14577
15317
  ...options.recipients ?? [],
14578
- ...options.recipientsFile !== void 0 ? readFileSync8(options.recipientsFile, "utf8").split(/[\r\n,]+/).map((id) => id.trim()).filter((id) => id.length > 0) : []
15318
+ ...options.recipientsFile !== void 0 ? readFileSync9(options.recipientsFile, "utf8").split(/[\r\n,]+/).map((id) => id.trim()).filter((id) => id.length > 0) : []
14579
15319
  ];
14580
15320
  if (recipients.length === 0) {
14581
15321
  throw new Error("Pass recipients via --recipients and/or --recipients-file.");
@@ -14585,7 +15325,7 @@ Next page: --cursor ${page.nextCursor}
14585
15325
  `Too many recipients: ${recipients.length} (max ${MAX_BROADCAST_RECIPIENTS} per broadcast).`
14586
15326
  );
14587
15327
  }
14588
- const content = resolveContent3(options);
15328
+ const content = resolveContent4(options);
14589
15329
  if (!options.confirm) {
14590
15330
  throw new Error(
14591
15331
  `Refusing to broadcast without --confirm. This sends a REAL DM to all ${recipients.length} listed client(s) and cannot be recalled once fanned out. Re-run with --confirm to proceed.`
@@ -14621,7 +15361,7 @@ Next page: --cursor ${page.nextCursor}
14621
15361
  }
14622
15362
 
14623
15363
  // src/commands/courses/index.ts
14624
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
15364
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
14625
15365
  var LESSON_STATUSES = ["draft", "public", "hidden", "locked"];
14626
15366
  var LESSON_TYPES = ["article", "video", "externalVideo", "task", "quiz"];
14627
15367
  var LEARNERS_SORTS = [
@@ -14738,7 +15478,7 @@ function parseLearnersSort(value) {
14738
15478
  }
14739
15479
  return match;
14740
15480
  }
14741
- function parsePositiveInt6(value, flag) {
15481
+ function parsePositiveInt7(value, flag) {
14742
15482
  const n = Number.parseInt(value, 10);
14743
15483
  if (!Number.isInteger(n) || n <= 0) {
14744
15484
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
@@ -14797,7 +15537,7 @@ function parseBool(value, flag) {
14797
15537
  }
14798
15538
  function parseJsonFileOrInline2(value, flag) {
14799
15539
  const trimmed = value.trim();
14800
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync9(value, "utf8");
15540
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync10(value, "utf8");
14801
15541
  try {
14802
15542
  return JSON.parse(raw);
14803
15543
  } catch (error) {
@@ -14890,7 +15630,7 @@ function registerCoursesCommands(program2) {
14890
15630
  const report = await notACourseGuard(
14891
15631
  productId,
14892
15632
  () => clientFor2(flags).getCourseAnalytics(productId, {
14893
- learnersLimit: options.learnersLimit !== void 0 ? parsePositiveInt6(options.learnersLimit, "--learners-limit") : void 0,
15633
+ learnersLimit: options.learnersLimit !== void 0 ? parsePositiveInt7(options.learnersLimit, "--learners-limit") : void 0,
14894
15634
  learnersSort: options.learnersSort !== void 0 ? parseLearnersSort(options.learnersSort) : void 0
14895
15635
  })
14896
15636
  );
@@ -15155,6 +15895,33 @@ Learners (${l.returned} of ${l.total}; progress basis = ${l.lessonsBasis} lesson
15155
15895
  });
15156
15896
  }
15157
15897
  );
15898
+ lesson.command("set-attachments").description(
15899
+ "Replace the lesson attachments (downloadable files) with media-library items, in the given order \u2014 pass the FULL list; `lesson get` shows the current uids"
15900
+ ).argument("<productId>", "course product id").argument("<lessonId>", "lesson id").option(
15901
+ "--media <mediaIds>",
15902
+ "comma-separated media-library ids (from `media upload` / `media list`), display order"
15903
+ ).option("--clear", "remove all attachments").action(
15904
+ async (productId, lessonId, options, command) => {
15905
+ const flags = getGlobalFlags(command);
15906
+ if (options.media === void 0 === !options.clear) {
15907
+ throw new Error("Pass exactly one of --media <id,id,...> or --clear.");
15908
+ }
15909
+ const attachmentMediaIds = options.clear ? [] : (options.media ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
15910
+ if (!options.clear && attachmentMediaIds.length === 0) {
15911
+ throw new Error("--media needs at least one media id (or use --clear).");
15912
+ }
15913
+ const result = await clientFor2(flags).updateCourseLesson(productId, lessonId, {
15914
+ attachmentMediaIds
15915
+ });
15916
+ emit(flags, result, () => {
15917
+ process.stdout.write(
15918
+ attachmentMediaIds.length === 0 ? `Cleared attachments of lesson ${result.lessonId}
15919
+ ` : `Set ${attachmentMediaIds.length} attachment(s) on lesson ${result.lessonId}
15920
+ `
15921
+ );
15922
+ });
15923
+ }
15924
+ );
15158
15925
  lesson.command("set-thumbnail").description(
15159
15926
  "Set or clear the lesson thumbnail; Bunny video lessons also get it as the player thumbnail"
15160
15927
  ).argument("<productId>", "course product id").argument("<lessonId>", "lesson id").option("--media <mediaId>", "media-library IMAGE id (from `media upload`)").option("--url <httpsUrl>", "already-hosted https image URL").option("--clear", "remove the thumbnail (video lessons fall back to the generated frame)").action(
@@ -15297,7 +16064,7 @@ ${NOT_SAVED_NOTE} --moments=${options.out ?? "<file>"} --confirm\`.
15297
16064
  if (options.content !== void 0 && options.contentFile !== void 0) {
15298
16065
  throw new Error("Use either --content or --content-file, not both.");
15299
16066
  }
15300
- const content = options.contentFile !== void 0 ? readFileSync9(options.contentFile, "utf8") : options.content;
16067
+ const content = options.contentFile !== void 0 ? readFileSync10(options.contentFile, "utf8") : options.content;
15301
16068
  if (options.title === void 0 && options.description === void 0 && options.status === void 0 && content === void 0) {
15302
16069
  throw new Error(
15303
16070
  "Nothing to update \u2014 pass at least one of --title, --description, --status, --content/--content-file."
@@ -15540,7 +16307,7 @@ ${NOT_SAVED_NOTE} --moments=${options.out ?? "<file>"} --confirm\`.
15540
16307
  }
15541
16308
 
15542
16309
  // src/commands/discounts/index.ts
15543
- function parsePositiveInt7(value, flag) {
16310
+ function parsePositiveInt8(value, flag) {
15544
16311
  const n = Number.parseInt(value, 10);
15545
16312
  if (!Number.isInteger(n) || n <= 0) {
15546
16313
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
@@ -15600,8 +16367,8 @@ function registerDiscountsCommands(program2) {
15600
16367
  throw new Error("--currency only applies with --amount-off.");
15601
16368
  }
15602
16369
  const percentOff = options.percentOff !== void 0 ? parsePercent(options.percentOff, "--percent-off") : void 0;
15603
- const amountOffCents = options.amountOff !== void 0 ? parsePositiveInt7(options.amountOff, "--amount-off") : void 0;
15604
- const maxRedemptions = options.maxRedemptions !== void 0 ? parsePositiveInt7(options.maxRedemptions, "--max-redemptions") : void 0;
16370
+ const amountOffCents = options.amountOff !== void 0 ? parsePositiveInt8(options.amountOff, "--amount-off") : void 0;
16371
+ const maxRedemptions = options.maxRedemptions !== void 0 ? parsePositiveInt8(options.maxRedemptions, "--max-redemptions") : void 0;
15605
16372
  const endsAt = options.ends !== void 0 ? parseIsoDate(options.ends, "--ends") : void 0;
15606
16373
  const startsAt = options.starts !== void 0 ? parseIsoDate(options.starts, "--starts") : void 0;
15607
16374
  const duration = options.forever ? "forever" : void 0;
@@ -15657,12 +16424,12 @@ function registerDiscountsCommands(program2) {
15657
16424
  input.percentOff = parsePercent(options.percentOff, "--percent-off");
15658
16425
  }
15659
16426
  if (options.amountOff !== void 0) {
15660
- input.amountOffCents = parsePositiveInt7(options.amountOff, "--amount-off");
16427
+ input.amountOffCents = parsePositiveInt8(options.amountOff, "--amount-off");
15661
16428
  }
15662
16429
  if (options.currency !== void 0) input.currency = options.currency;
15663
16430
  if (options.product.length > 0) input.productIds = options.product;
15664
16431
  if (options.maxRedemptions !== void 0) {
15665
- input.maxRedemptions = parsePositiveInt7(options.maxRedemptions, "--max-redemptions");
16432
+ input.maxRedemptions = parsePositiveInt8(options.maxRedemptions, "--max-redemptions");
15666
16433
  }
15667
16434
  if (options.clearMaxRedemptions) input.maxRedemptions = null;
15668
16435
  if (options.ends !== void 0) input.endsAt = parseIsoDate(options.ends, "--ends");
@@ -15722,45 +16489,268 @@ function formatDiscountValue(percentOff, amountOffCents, currency) {
15722
16489
  }
15723
16490
 
15724
16491
  // src/commands/emails/index.ts
15725
- import { readFileSync as readFileSync10 } from "fs";
15726
- var EMAIL_LANGUAGES = ["en", "pl"];
15727
- var TEXT_SUMMARY_LENGTH = 60;
15728
- function orNull2(value) {
15729
- return value.trim().toLowerCase() === "none" ? null : value;
16492
+ import { readFileSync as readFileSync12 } from "fs";
16493
+
16494
+ // src/commands/emails/library.ts
16495
+ import { readFileSync as readFileSync11 } from "fs";
16496
+ function resolveContent5(options) {
16497
+ if (options.content !== void 0 && options.contentFile !== void 0) {
16498
+ throw new Error("Use either --content or --content-file, not both.");
16499
+ }
16500
+ return options.contentFile !== void 0 ? readFileSync11(options.contentFile, "utf8") : options.content;
15730
16501
  }
15731
- function parseLanguage(value, fallback) {
15732
- if (value === void 0) return fallback;
15733
- const normalized = value.trim().toLowerCase();
15734
- if (!EMAIL_LANGUAGES.includes(normalized)) {
15735
- throw new Error(`Invalid --lang: ${value} (expected ${EMAIL_LANGUAGES.join(" | ")})`);
16502
+ function resolveBranded2(options) {
16503
+ if (options.branded && options.plain) {
16504
+ throw new Error("Use either --branded or --plain, not both.");
15736
16505
  }
15737
- return normalized;
16506
+ if (options.branded) return true;
16507
+ if (options.plain) return false;
16508
+ return void 0;
15738
16509
  }
15739
- function pickText(text, language) {
15740
- if (!text) return "";
15741
- const other = language === "en" ? "pl" : "en";
15742
- return text[language] ?? text[other] ?? "";
16510
+ function resolveNullable(value, clear, flag) {
16511
+ if (value !== void 0 && clear) {
16512
+ throw new Error(`Use either --${flag} or --clear-${flag}, not both.`);
16513
+ }
16514
+ return clear ? null : value;
15743
16515
  }
15744
- function truncate2(value, max) {
15745
- const singleLine = value.replace(/\s+/g, " ").trim();
15746
- return singleLine.length > max ? `${singleLine.slice(0, max - 1)}\u2026` : singleLine;
16516
+ function buildFields(options) {
16517
+ const input = {};
16518
+ if (options.name !== void 0) input.name = options.name;
16519
+ if (options.subject !== void 0) input.subject = options.subject;
16520
+ const preheader = resolveNullable(options.preheader, options.clearPreheader, "preheader");
16521
+ if (preheader !== void 0) input.preheader = preheader;
16522
+ const contentMarkdown = resolveContent5(options);
16523
+ if (contentMarkdown !== void 0) input.contentMarkdown = contentMarkdown;
16524
+ const branded = resolveBranded2(options);
16525
+ if (branded !== void 0) input.branded = branded;
16526
+ const fromName = resolveNullable(options.fromName, options.clearFromName, "from-name");
16527
+ if (fromName !== void 0) input.fromName = fromName;
16528
+ const replyTo = resolveNullable(options.replyTo, options.clearReplyTo, "reply-to");
16529
+ if (replyTo !== void 0) input.replyTo = replyTo;
16530
+ if (options.clearAttachments) input.attachments = [];
16531
+ return input;
15747
16532
  }
15748
- function parseLayout(value) {
15749
- const trimmed = value.trim();
15750
- const raw = trimmed.startsWith("[") ? trimmed : readFileSync10(value, "utf8");
15751
- let parsed;
15752
- try {
15753
- parsed = JSON.parse(raw);
15754
- } catch (error) {
15755
- throw new Error(`Invalid JSON for --layout: ${error.message}`);
15756
- }
15757
- if (!Array.isArray(parsed)) {
15758
- throw new Error("--layout must be a JSON array of email blocks (the full layout).");
15759
- }
15760
- return parsed;
16533
+ function describeAttachment(attachment) {
16534
+ return attachment.kind === "legal-document" ? `${attachment.document} (PDF rendered at send time)` : `${attachment.name} (media ${attachment.mediaId})`;
15761
16535
  }
15762
- function findEmail(templates, key) {
15763
- const email = templates.emails.find((entry) => entry.key === key);
16536
+ function printLibraryEmail(email) {
16537
+ process.stdout.write(`E-mail template ${email.id}
16538
+ `);
16539
+ process.stdout.write(` Name: ${email.name}
16540
+ `);
16541
+ process.stdout.write(` Subject: ${email.subject || "(none)"}
16542
+ `);
16543
+ process.stdout.write(` Preheader: ${email.preheader ?? "(none)"}
16544
+ `);
16545
+ process.stdout.write(` Frame: ${email.branded ? "branded" : "plain"}
16546
+ `);
16547
+ process.stdout.write(` From name: ${email.fromName ?? "(workspace default)"}
16548
+ `);
16549
+ process.stdout.write(` Reply-To: ${email.replyTo ?? "(workspace default)"}
16550
+ `);
16551
+ process.stdout.write(
16552
+ ` Attachments: ${email.attachments.length > 0 ? email.attachments.map(describeAttachment).join(", ") : "(none)"}
16553
+ `
16554
+ );
16555
+ process.stdout.write(` Updated at: ${email.updatedAt}
16556
+ `);
16557
+ process.stdout.write(` Content (markdown, ${email.contentMarkdown.length} chars):
16558
+ `);
16559
+ if (email.contentMarkdown) {
16560
+ for (const line of email.contentMarkdown.split("\n")) process.stdout.write(` ${line}
16561
+ `);
16562
+ }
16563
+ }
16564
+ var FIELD_FLAGS_HELP = "Attachments other than clearing them are not settable from flags \u2014 attach files in the dashboard.";
16565
+ function registerEmailLibraryCommands(emails) {
16566
+ const library = emails.command("library").description(
16567
+ 'The reusable e-mail template library (Marketing \u2192 E-mail templates) that workflow "Send email" steps reference \u2014 not the system e-mails above'
16568
+ );
16569
+ library.command("list").description("List library e-mail templates").action(async (_, command) => {
16570
+ const flags = getGlobalFlags(command);
16571
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
16572
+ const page = await client.listLibraryEmails();
16573
+ emit(flags, page, () => {
16574
+ printTable(
16575
+ page.emails.map((e) => ({
16576
+ id: e.id,
16577
+ name: e.name,
16578
+ subject: e.subject,
16579
+ frame: e.branded ? "branded" : "plain",
16580
+ attachments: e.attachments.length,
16581
+ updatedAt: e.updatedAt
16582
+ })),
16583
+ ["id", "name", "subject", "frame", "attachments", "updatedAt"]
16584
+ );
16585
+ });
16586
+ });
16587
+ library.command("get").description("Show one library e-mail template (subject, sender, attachments, content)").argument("<id>", "e-mail template id").action(async (id, _options, command) => {
16588
+ const flags = getGlobalFlags(command);
16589
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
16590
+ const result = await client.getLibraryEmail(id);
16591
+ emit(flags, result, () => printLibraryEmail(result.email));
16592
+ });
16593
+ library.command("create").description(`Create a library e-mail template. ${FIELD_FLAGS_HELP}`).requiredOption("--name <name>", "internal label shown in the library list (not the subject)").option("--subject <subject>", "e-mail subject line").option("--preheader <text>", "preview text shown after the subject in inboxes").option("--content <markdown>", "e-mail body as Markdown").option("--content-file <path>", "read the e-mail body (Markdown) from a file").option("--branded", "wrap the e-mail in the workspace's branded frame").option("--plain", "send it as a plain, personal-looking e-mail").option("--from-name <name>", "From display name override").option("--reply-to <email>", "Reply-To address override").action(async (options, command) => {
16594
+ const flags = getGlobalFlags(command);
16595
+ const fields = buildFields(options);
16596
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
16597
+ const result = await client.createLibraryEmail({ ...fields, name: options.name });
16598
+ emit(flags, result, () => {
16599
+ process.stdout.write(
16600
+ `Created e-mail template ${result.email.id} (${result.email.name}).
16601
+ `
16602
+ );
16603
+ });
16604
+ });
16605
+ library.command("update").description(
16606
+ `Update a library e-mail template \u2014 workflows that reference it send the new version from now on. ${FIELD_FLAGS_HELP}`
16607
+ ).argument("<id>", "e-mail template id").option("--name <name>", "new internal label").option("--subject <subject>", "new subject line").option("--preheader <text>", "new preview text").option("--clear-preheader", "remove the preview text").option("--content <markdown>", "new body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new body (Markdown) from a file").option("--branded", "wrap the e-mail in the workspace's branded frame").option("--plain", "send it as a plain, personal-looking e-mail").option("--from-name <name>", "new From display name override").option("--clear-from-name", "go back to the workspace default From name").option("--reply-to <email>", "new Reply-To address override").option("--clear-reply-to", "go back to the workspace default Reply-To").option("--clear-attachments", "remove every attachment").action(async (id, options, command) => {
16608
+ const flags = getGlobalFlags(command);
16609
+ const input = buildFields(options);
16610
+ if (Object.keys(input).length === 0) {
16611
+ throw new Error(
16612
+ "Nothing to update \u2014 pass at least one change flag (see `emails library update --help`)."
16613
+ );
16614
+ }
16615
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
16616
+ const result = await client.updateLibraryEmail(id, input);
16617
+ emit(flags, result, () => {
16618
+ process.stdout.write(`Updated e-mail template ${result.email.id}.
16619
+ `);
16620
+ });
16621
+ });
16622
+ library.command("duplicate").description("Duplicate a library e-mail template (the copy is independent of the original)").argument("<id>", "e-mail template id").action(async (id, _options, command) => {
16623
+ const flags = getGlobalFlags(command);
16624
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
16625
+ const result = await client.duplicateLibraryEmail(id);
16626
+ emit(flags, result, () => {
16627
+ process.stdout.write(`Duplicated ${id} \u2192 ${result.email.id} (${result.email.name}).
16628
+ `);
16629
+ });
16630
+ });
16631
+ library.command("delete").description(
16632
+ "PERMANENTLY delete a library e-mail template \u2014 cannot be undone; a workflow step that still references it stops sending until it is re-pointed. Requires --confirm"
16633
+ ).argument("<id>", "e-mail template id").option("--confirm", "required \u2014 acknowledge the permanent deletion").action(async (id, options, command) => {
16634
+ const flags = getGlobalFlags(command);
16635
+ if (!options.confirm) {
16636
+ throw new Error(
16637
+ `Refusing to delete e-mail template ${id} without --confirm. Re-run with --confirm to proceed.`
16638
+ );
16639
+ }
16640
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
16641
+ const result = await client.deleteLibraryEmail(id);
16642
+ emit(flags, result, () => {
16643
+ process.stdout.write(`Deleted e-mail template ${id}.
16644
+ `);
16645
+ });
16646
+ });
16647
+ }
16648
+
16649
+ // src/commands/emails/sender.ts
16650
+ function printSender(sender) {
16651
+ const defaultFrom = sender.senderDomain?.defaultFromEmail ?? sender.platformFromAddress;
16652
+ process.stdout.write(`Default From: ${defaultFrom}`);
16653
+ process.stdout.write(sender.senderDomain ? "\n" : " (platform sender \u2014 no custom domain)\n");
16654
+ process.stdout.write(
16655
+ `Footer postal address: ${sender.postalAddress ?? "(not set \u2014 campaign sends require one)"}
16656
+ `
16657
+ );
16658
+ process.stdout.write(`Unsubscribe label: ${sender.unsubscribeLabel ?? "(language default)"}
16659
+ `);
16660
+ process.stdout.write(
16661
+ `System e-mails from the custom domain: ${sender.systemEmailsFromCustomDomain ? "yes" : "no"}
16662
+ `
16663
+ );
16664
+ process.stdout.write(
16665
+ `Sending domains: ${sender.senderDomains.length} of ${sender.maxSenderDomains}
16666
+ `
16667
+ );
16668
+ if (sender.senderDomains.length > 0) {
16669
+ printTable(
16670
+ sender.senderDomains.map((d) => ({
16671
+ domain: d.domain,
16672
+ default: d.isDefault ? "yes" : "",
16673
+ status: d.status,
16674
+ mailFrom: d.mailFromStatus,
16675
+ from: d.defaultFromEmail,
16676
+ verifiedAt: d.verifiedAt ?? ""
16677
+ })),
16678
+ ["domain", "default", "status", "mailFrom", "from", "verifiedAt"]
16679
+ );
16680
+ }
16681
+ }
16682
+ function registerEmailSenderCommands(emails) {
16683
+ const sender = emails.command("sender").description(
16684
+ "The sending identity behind campaigns: verified sending domains, the default From and the footer postal address. Adding / verifying / deleting a domain is dashboard-only (DNS)"
16685
+ );
16686
+ sender.command("get").description("Show the sending domains, default From address and footer postal address").action(async (_, command) => {
16687
+ const flags = getGlobalFlags(command);
16688
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
16689
+ const result = await client.getSenderDomain();
16690
+ emit(flags, result, () => printSender(result));
16691
+ });
16692
+ sender.command("set-address").description(
16693
+ 'Set the footer postal address rendered under the unsubscribe link of every campaign (one plain-text line, e.g. "Company, street, city, country") \u2014 required before a send'
16694
+ ).option("--address <line>", "the postal address line (max 300 chars)").option("--clear", "remove the postal address").action(async (options, command) => {
16695
+ const flags = getGlobalFlags(command);
16696
+ if (options.address !== void 0 && options.clear) {
16697
+ throw new Error("Use either --address or --clear, not both.");
16698
+ }
16699
+ if (options.address === void 0 && !options.clear) {
16700
+ throw new Error("Pass --address <line> to set it or --clear to remove it.");
16701
+ }
16702
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
16703
+ const result = await client.updateSenderPostalAddress(
16704
+ options.clear ? null : options.address ?? null
16705
+ );
16706
+ emit(flags, result, () => {
16707
+ process.stdout.write(
16708
+ result.postalAddress ? `Footer postal address set to: ${result.postalAddress}
16709
+ ` : "Footer postal address cleared.\n"
16710
+ );
16711
+ });
16712
+ });
16713
+ }
16714
+
16715
+ // src/commands/emails/index.ts
16716
+ var EMAIL_LANGUAGES = ["en", "pl"];
16717
+ var TEXT_SUMMARY_LENGTH = 60;
16718
+ function orNull2(value) {
16719
+ return value.trim().toLowerCase() === "none" ? null : value;
16720
+ }
16721
+ function parseLanguage(value, fallback) {
16722
+ if (value === void 0) return fallback;
16723
+ const normalized = value.trim().toLowerCase();
16724
+ if (!EMAIL_LANGUAGES.includes(normalized)) {
16725
+ throw new Error(`Invalid --lang: ${value} (expected ${EMAIL_LANGUAGES.join(" | ")})`);
16726
+ }
16727
+ return normalized;
16728
+ }
16729
+ function pickText(text, language) {
16730
+ if (!text) return "";
16731
+ const other = language === "en" ? "pl" : "en";
16732
+ return text[language] ?? text[other] ?? "";
16733
+ }
16734
+ function truncate2(value, max) {
16735
+ const singleLine = value.replace(/\s+/g, " ").trim();
16736
+ return singleLine.length > max ? `${singleLine.slice(0, max - 1)}\u2026` : singleLine;
16737
+ }
16738
+ function parseLayout(value) {
16739
+ const trimmed = value.trim();
16740
+ const raw = trimmed.startsWith("[") ? trimmed : readFileSync12(value, "utf8");
16741
+ let parsed;
16742
+ try {
16743
+ parsed = JSON.parse(raw);
16744
+ } catch (error) {
16745
+ throw new Error(`Invalid JSON for --layout: ${error.message}`);
16746
+ }
16747
+ if (!Array.isArray(parsed)) {
16748
+ throw new Error("--layout must be a JSON array of email blocks (the full layout).");
16749
+ }
16750
+ return parsed;
16751
+ }
16752
+ function findEmail(templates, key) {
16753
+ const email = templates.emails.find((entry) => entry.key === key);
15764
16754
  if (!email) {
15765
16755
  const known = templates.emails.map((entry) => entry.key).join(", ");
15766
16756
  throw new Error(`Unknown email "${key}". Known keys: ${known}`);
@@ -15783,7 +16773,7 @@ function summarizeBlock(block, language) {
15783
16773
  return "";
15784
16774
  }
15785
16775
  }
15786
- function printEmailDetail(email, language) {
16776
+ function printEmailDetail2(email, language) {
15787
16777
  printTable(
15788
16778
  [
15789
16779
  { field: "key", value: email.key },
@@ -15896,7 +16886,7 @@ function registerEmailsCommands(program2) {
15896
16886
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
15897
16887
  const templates = await client.getEmailTemplates();
15898
16888
  const email = findEmail(templates, key);
15899
- emit(flags, email, () => printEmailDetail(email, templates.defaults.language));
16889
+ emit(flags, email, () => printEmailDetail2(email, templates.defaults.language));
15900
16890
  });
15901
16891
  emails.command("set <key>").description(
15902
16892
  "Customize one system email. Text flags take 'none' to clear; --layout replaces the whole block layout (inline JSON array or .json path); --reset restores the stock email."
@@ -15970,10 +16960,12 @@ function registerEmailsCommands(program2) {
15970
16960
  `);
15971
16961
  });
15972
16962
  });
16963
+ registerEmailLibraryCommands(emails);
16964
+ registerEmailSenderCommands(emails);
15973
16965
  }
15974
16966
 
15975
16967
  // src/commands/forms/index.ts
15976
- import { readFileSync as readFileSync11 } from "fs";
16968
+ import { readFileSync as readFileSync13 } from "fs";
15977
16969
  var OPT_IN_MODES = ["double", "single"];
15978
16970
  var FORM_VARIANTS = [
15979
16971
  "basic",
@@ -15990,7 +16982,7 @@ function parseChoice2(value, allowed, flag) {
15990
16982
  }
15991
16983
  return match;
15992
16984
  }
15993
- function parsePositiveInt8(value, flag) {
16985
+ function parsePositiveInt9(value, flag) {
15994
16986
  const n = Number.parseInt(value, 10);
15995
16987
  if (!Number.isInteger(n) || n <= 0) {
15996
16988
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
@@ -16005,13 +16997,16 @@ function parseCsv2(value) {
16005
16997
  }
16006
16998
  function parseJsonFileOrInline3(value, flag) {
16007
16999
  const trimmed = value.trim();
16008
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync11(value, "utf8");
17000
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync13(value, "utf8");
16009
17001
  try {
16010
17002
  return JSON.parse(raw);
16011
17003
  } catch (error) {
16012
17004
  throw new Error(`Invalid JSON for ${flag}: ${error.message}`);
16013
17005
  }
16014
17006
  }
17007
+ var PHONE_FIELD_MODES = ["off", "optional", "required"];
17008
+ var ON_OFF = ["on", "off"];
17009
+ var MAX_SMS_CONSENT_TEXT_LENGTH = 300;
16015
17010
  function buildConfigOverrides(options) {
16016
17011
  const overrides = {};
16017
17012
  if (options.header !== void 0) overrides.headerText = options.header;
@@ -16021,8 +17016,32 @@ function buildConfigOverrides(options) {
16021
17016
  if (options.variant !== void 0) {
16022
17017
  overrides.variant = parseChoice2(options.variant, FORM_VARIANTS, "--variant");
16023
17018
  }
17019
+ if (options.phone !== void 0) {
17020
+ const mode = parseChoice2(options.phone, PHONE_FIELD_MODES, "--phone");
17021
+ overrides.showPhoneNumberInput = mode !== "off";
17022
+ overrides.phoneNumberRequired = mode === "required";
17023
+ if (mode === "off") overrides.showSmsConsentCheckbox = false;
17024
+ }
17025
+ if (options.smsConsent !== void 0) {
17026
+ overrides.showSmsConsentCheckbox = parseChoice2(options.smsConsent, ON_OFF, "--sms-consent") === "on";
17027
+ }
17028
+ if (options.smsConsentText !== void 0) {
17029
+ if (options.smsConsentText.length > MAX_SMS_CONSENT_TEXT_LENGTH) {
17030
+ throw new Error(
17031
+ `--sms-consent-text is limited to ${MAX_SMS_CONSENT_TEXT_LENGTH} characters.`
17032
+ );
17033
+ }
17034
+ overrides.smsConsentText = options.smsConsentText;
17035
+ }
16024
17036
  return Object.keys(overrides).length > 0 ? overrides : void 0;
16025
17037
  }
17038
+ function assertSmsConsentHasPhoneField(config) {
17039
+ if (config?.showSmsConsentCheckbox === true && config.showPhoneNumberInput !== true) {
17040
+ throw new Error(
17041
+ "--sms-consent on needs the phone field: add --phone optional or --phone required."
17042
+ );
17043
+ }
17044
+ }
16026
17045
  function parseFormJsonPatch(value) {
16027
17046
  const parsed = parseJsonFileOrInline3(value, "--json");
16028
17047
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
@@ -16084,6 +17103,7 @@ async function buildFormUpdateInput(client, formId, options) {
16084
17103
  if (configOverrides !== void 0) {
16085
17104
  const base = input.config ?? (await fetchCurrent()).config;
16086
17105
  input.config = { ...base, ...configOverrides };
17106
+ assertSmsConsentHasPhoneField(input.config);
16087
17107
  }
16088
17108
  if (options.clearConfirmationEmail) {
16089
17109
  if (options.confirmationSubject !== void 0 || options.confirmationIntro !== void 0) {
@@ -16135,19 +17155,30 @@ function registerFormsCommands(program2) {
16135
17155
  emit(flags, form, () => printSignupFormDetail(form));
16136
17156
  });
16137
17157
  forms.command("create").description(
16138
- "Create a standalone signup form (double opt-in by default \u2014 switch opt-in, redirect and confirmation-email copy with `forms update`)"
16139
- ).requiredOption("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids applied on (confirmed) submission").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(" | ")})`).action(
17158
+ "Create a standalone signup form (double opt-in unless --opt-in single). The full confirmation-email copy is set with `forms update --json`"
17159
+ ).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(
17160
+ "--sms-consent <on|off>",
17161
+ "config: marketing-SMS consent checkbox (needs the phone field)"
17162
+ ).option(
17163
+ "--sms-consent-text <text>",
17164
+ "config: consent wording next to the checkbox (max 300 chars)"
17165
+ ).action(
16140
17166
  async (options, command) => {
16141
17167
  const flags = getGlobalFlags(command);
17168
+ const optInMode = options.optIn !== void 0 ? parseChoice2(options.optIn, OPT_IN_MODES, "--opt-in") : void 0;
17169
+ const config = buildConfigOverrides(options);
17170
+ assertSmsConsentHasPhoneField(config);
16142
17171
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
16143
17172
  const result = await client.createSignupForm({
16144
17173
  name: options.name,
16145
17174
  tagIds: options.tags !== void 0 ? parseCsv2(options.tags) : void 0,
16146
- config: buildConfigOverrides(options)
17175
+ config,
17176
+ ...optInMode !== void 0 ? { optInMode } : {},
17177
+ ...options.redirect !== void 0 ? { redirectUrl: options.redirect } : {}
16147
17178
  });
16148
17179
  emit(flags, result, () => {
16149
17180
  process.stdout.write(
16150
- `Created signup form ${result.formId} (${options.name}) \u2014 double opt-in by default.
17181
+ `Created signup form ${result.formId} (${options.name}) \u2014 ${optInMode ?? "double"} opt-in.
16151
17182
  `
16152
17183
  );
16153
17184
  });
@@ -16155,7 +17186,13 @@ function registerFormsCommands(program2) {
16155
17186
  );
16156
17187
  forms.command("update").description(
16157
17188
  "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."
16158
- ).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(
17189
+ ).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(
17190
+ "--sms-consent <on|off>",
17191
+ "config: marketing-SMS consent checkbox (needs the phone field)"
17192
+ ).option(
17193
+ "--sms-consent-text <text>",
17194
+ "config: consent wording next to the checkbox (max 300 chars)"
17195
+ ).option(
16159
17196
  "--json <fileOrJson>",
16160
17197
  "inline JSON object or .json path merged into the request body \u2014 for deep fields (config, localizedConfig, \u2026). Explicit flags win over --json keys."
16161
17198
  ).action(async (id, options, command) => {
@@ -16189,7 +17226,7 @@ function registerFormsCommands(program2) {
16189
17226
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
16190
17227
  const result = await client.listSignupFormSubmissions(
16191
17228
  id,
16192
- options.limit !== void 0 ? parsePositiveInt8(options.limit, "--limit") : void 0
17229
+ options.limit !== void 0 ? parsePositiveInt9(options.limit, "--limit") : void 0
16193
17230
  );
16194
17231
  emit(flags, result, () => {
16195
17232
  printTable(
@@ -16207,7 +17244,7 @@ function registerFormsCommands(program2) {
16207
17244
  }
16208
17245
 
16209
17246
  // src/commands/funnels/index.ts
16210
- import { readFileSync as readFileSync12 } from "fs";
17247
+ import { readFileSync as readFileSync14 } from "fs";
16211
17248
  var CREATABLE_RECIPES = ["lead_magnet", "tripwire", "sell_product"];
16212
17249
  var FUNNEL_STATUSES = ["draft", "live", "archived"];
16213
17250
  var FUNNEL_LANGS = ["en", "pl"];
@@ -16215,12 +17252,13 @@ var FUNNEL_TEMPLATE_IDS = [
16215
17252
  "squeeze",
16216
17253
  "tripwire",
16217
17254
  "sales_oto",
17255
+ "low_ticket",
16218
17256
  "webinar",
16219
17257
  "challenge"
16220
17258
  ];
16221
17259
  var EVENT_MODES = ["live", "on_demand"];
16222
17260
  var SCHEDULER_UNAVAILABLE_STATUS2 = 503;
16223
- function requireConfirm5(confirm, what) {
17261
+ function requireConfirm6(confirm, what) {
16224
17262
  if (!confirm) {
16225
17263
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
16226
17264
  }
@@ -16235,7 +17273,7 @@ function withSchedulerHint2(error) {
16235
17273
  }
16236
17274
  throw error;
16237
17275
  }
16238
- function parsePositiveInt9(value, flag) {
17276
+ function parsePositiveInt10(value, flag) {
16239
17277
  const n = Number.parseInt(value, 10);
16240
17278
  if (!Number.isInteger(n) || n <= 0) {
16241
17279
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
@@ -16272,7 +17310,7 @@ function parseCsv3(value) {
16272
17310
  }
16273
17311
  function parseJsonFileOrInline4(value, flag) {
16274
17312
  const trimmed = value.trim();
16275
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync12(value, "utf8");
17313
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync14(value, "utf8");
16276
17314
  try {
16277
17315
  return JSON.parse(raw);
16278
17316
  } catch (error) {
@@ -16495,7 +17533,7 @@ function registerFunnelsCommands(program2) {
16495
17533
  ).argument("<templateId>", `template id (${FUNNEL_TEMPLATE_IDS.join(" | ")})`).requiredOption("--product <productId>", "the product the funnel sells or delivers").requiredOption("--lang <en|pl>", "language of the generated pages and emails \u2014 the creator's").option("--name <name>", "custom funnel name; omit for the recipe default").option("--event-start <iso>", "webinar: event start as ISO 8601 WITH offset (live)").option("--event-timezone <tz>", "webinar: IANA timezone, e.g. Europe/Warsaw").option("--event-duration <minutes>", "webinar: event length in minutes (default 60)").option("--event-mode <mode>", `webinar: ${EVENT_MODES.join(" | ")} (on_demand = autowebinar)`).option("--confirm", "required \u2014 acknowledge that AI credits will be consumed").action(
16496
17534
  async (templateId, options, command) => {
16497
17535
  const flags = getGlobalFlags(command);
16498
- requireConfirm5(options.confirm, "create the funnel and generate its pages (AI credits)");
17536
+ requireConfirm6(options.confirm, "create the funnel and generate its pages (AI credits)");
16499
17537
  const input = {
16500
17538
  templateId: parseChoice3(templateId, FUNNEL_TEMPLATE_IDS, "<templateId>"),
16501
17539
  productId: options.product,
@@ -16505,7 +17543,7 @@ function registerFunnelsCommands(program2) {
16505
17543
  const webinar = {
16506
17544
  eventStartAt: options.eventStart,
16507
17545
  eventTimezone: options.eventTimezone,
16508
- eventDurationMinutes: options.eventDuration !== void 0 ? parsePositiveInt9(options.eventDuration, "--event-duration") : void 0,
17546
+ eventDurationMinutes: options.eventDuration !== void 0 ? parsePositiveInt10(options.eventDuration, "--event-duration") : void 0,
16509
17547
  eventMode: options.eventMode !== void 0 ? parseChoice3(options.eventMode, EVENT_MODES, "--event-mode") : void 0
16510
17548
  };
16511
17549
  if (Object.values(webinar).some((value) => value !== void 0)) {
@@ -16753,7 +17791,7 @@ join again to refresh it.
16753
17791
  }
16754
17792
 
16755
17793
  // src/commands/media/index.ts
16756
- import { existsSync, readFileSync as readFileSync13, writeFileSync as writeFileSync4 } from "fs";
17794
+ import { existsSync, readFileSync as readFileSync15, writeFileSync as writeFileSync4 } from "fs";
16757
17795
  import { readFile, stat as stat3 } from "fs/promises";
16758
17796
  import { basename } from "path";
16759
17797
 
@@ -19172,11 +20210,11 @@ function _readChunk() {
19172
20210
  reject(err);
19173
20211
  };
19174
20212
  var onReadable = function onReadable2() {
19175
- var chunk = stream.read(size);
19176
- if (chunk !== null) {
20213
+ var chunk2 = stream.read(size);
20214
+ if (chunk2 !== null) {
19177
20215
  stream.off("error", onError);
19178
20216
  stream.off("readable", onReadable2);
19179
- resolve2(chunk);
20217
+ resolve2(chunk2);
19180
20218
  }
19181
20219
  };
19182
20220
  stream.once("error", onError);
@@ -19658,10 +20696,10 @@ var ProgressEmitter = /* @__PURE__ */ (function(_Transform) {
19658
20696
  _inherits2(ProgressEmitter2, _Transform);
19659
20697
  return _createClass8(ProgressEmitter2, [{
19660
20698
  key: "_transform",
19661
- value: function _transform(chunk, _encoding, callback) {
19662
- this._position += chunk.length;
20699
+ value: function _transform(chunk2, _encoding, callback) {
20700
+ this._position += chunk2.length;
19663
20701
  this._onprogress(this._position);
19664
- callback(null, chunk);
20702
+ callback(null, chunk2);
19665
20703
  }
19666
20704
  }]);
19667
20705
  })(Transform);
@@ -19673,9 +20711,9 @@ var writeBufferToStreamWithProgress = function writeBufferToStreamWithProgress2(
19673
20711
  var offset = 0;
19674
20712
  function writeNextChunk() {
19675
20713
  var chunkSize = Math.min(stream.writableHighWaterMark, source.length - offset);
19676
- var chunk = source.subarray(offset, offset + chunkSize);
19677
- offset += chunk.length;
19678
- var canContinue = stream.write(chunk);
20714
+ var chunk2 = source.subarray(offset, offset + chunkSize);
20715
+ offset += chunk2.length;
20716
+ var canContinue = stream.write(chunk2);
19679
20717
  if (!canContinue) {
19680
20718
  stream.once("drain", writeNextChunk);
19681
20719
  onprogress(offset);
@@ -19874,13 +20912,13 @@ async function uploadVideoToBunny(params) {
19874
20912
  // src/commands/media/index.ts
19875
20913
  var DEFAULT_AI_IMAGE_ASPECT = "1:1" /* Square */;
19876
20914
  var DEFAULT_AI_IMAGE_QUALITY = "1K" /* Standard */;
19877
- function requireConfirm6(confirm, what) {
20915
+ function requireConfirm7(confirm, what) {
19878
20916
  if (!confirm) {
19879
20917
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
19880
20918
  }
19881
20919
  }
19882
20920
  function resolveTextOrFile(value) {
19883
- const text = existsSync(value) ? readFileSync13(value, "utf8") : value;
20921
+ const text = existsSync(value) ? readFileSync15(value, "utf8") : value;
19884
20922
  if (!text.trim()) throw new Error("The text (or the file it points to) is empty.");
19885
20923
  return text.trim();
19886
20924
  }
@@ -20185,7 +21223,7 @@ function registerMediaCommands(program2) {
20185
21223
  ).option("--folder <name>", "media library folder name to file the image into").option("--text-language <code>", "language of any text rendered inside the image (ISO code)").option("--confirm", "required \u2014 acknowledge spending AI credits").action(
20186
21224
  async (options, command) => {
20187
21225
  const flags = getGlobalFlags(command);
20188
- requireConfirm6(options.confirm, "generate an AI image (spends AI credits)");
21226
+ requireConfirm7(options.confirm, "generate an AI image (spends AI credits)");
20189
21227
  const input = {
20190
21228
  prompt: resolveTextOrFile(options.prompt),
20191
21229
  aspectRatio: parseEnumOption(
@@ -20217,7 +21255,7 @@ function registerMediaCommands(program2) {
20217
21255
  ).argument("<mediaId>", "video media item id").option("--language <code>", "spoken language hint (ISO code); omit for auto-detect").option("--force", "re-transcribe even when a transcript already exists").option("--confirm", "required \u2014 acknowledge spending transcription minutes").action(
20218
21256
  async (mediaId, options, command) => {
20219
21257
  const flags = getGlobalFlags(command);
20220
- requireConfirm6(options.confirm, `transcribe ${mediaId} (spends transcription minutes)`);
21258
+ requireConfirm7(options.confirm, `transcribe ${mediaId} (spends transcription minutes)`);
20221
21259
  const input = {};
20222
21260
  if (options.language !== void 0) input.language = options.language;
20223
21261
  if (options.force) input.force = true;
@@ -20291,13 +21329,13 @@ function registerMediaCommands(program2) {
20291
21329
  }
20292
21330
 
20293
21331
  // src/commands/newsletters/index.ts
20294
- import { existsSync as existsSync2, readFileSync as readFileSync14 } from "fs";
21332
+ import { existsSync as existsSync2, readFileSync as readFileSync16 } from "fs";
20295
21333
  var ISSUE_STATUSES = ["public", "hidden"];
20296
21334
  var ISSUE_VISIBILITIES = ["public", "hidden", "locked"];
20297
21335
  var ISSUE_RENDER_MODES = ["snapshot", "article"];
20298
21336
  var THUMBNAIL_NONE = "none";
20299
21337
  function resolveMarkdownOrFile(value) {
20300
- return existsSync2(value) ? readFileSync14(value, "utf8") : value;
21338
+ return existsSync2(value) ? readFileSync16(value, "utf8") : value;
20301
21339
  }
20302
21340
  function parseIdList2(value, flag) {
20303
21341
  const ids = value.split(",").map((id) => id.trim()).filter(Boolean);
@@ -20316,7 +21354,7 @@ function assertOneOf2(value, allowed, flag) {
20316
21354
  }
20317
21355
  return value;
20318
21356
  }
20319
- function parsePositiveInt10(value, flag) {
21357
+ function parsePositiveInt11(value, flag) {
20320
21358
  const n = Number.parseInt(value, 10);
20321
21359
  if (!Number.isInteger(n) || n <= 0) {
20322
21360
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
@@ -20338,7 +21376,7 @@ function parseBool2(value, flag) {
20338
21376
  }
20339
21377
  function parseJsonFileOrInline5(value, flag) {
20340
21378
  const trimmed = value.trim();
20341
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync14(value, "utf8");
21379
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync16(value, "utf8");
20342
21380
  try {
20343
21381
  return JSON.parse(raw);
20344
21382
  } catch (error) {
@@ -20356,7 +21394,7 @@ function registerNewslettersCommands(program2) {
20356
21394
  const flags = getGlobalFlags(command);
20357
21395
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
20358
21396
  const page = await client.listNewsletterIssues(options.product, {
20359
- limit: options.limit !== void 0 ? parsePositiveInt10(options.limit, "--limit") : void 0,
21397
+ limit: options.limit !== void 0 ? parsePositiveInt11(options.limit, "--limit") : void 0,
20360
21398
  cursor: options.cursor
20361
21399
  });
20362
21400
  emit(flags, page, () => {
@@ -20653,9 +21691,10 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
20653
21691
  }
20654
21692
 
20655
21693
  // src/commands/offers/index.ts
20656
- import { readFileSync as readFileSync15 } from "fs";
21694
+ import { readFileSync as readFileSync17 } from "fs";
20657
21695
  var PRICE_BILLING_TYPES = ["one_time", "recurring", "installments"];
20658
21696
  var PRICE_INTERVALS = ["day", "week", "month", "year"];
21697
+ var SALE_START_DISPLAYS = ["hidden", "countdown"];
20659
21698
  var PRICE_CURRENCIES = ["pln", "usd", "eur", "gbp"];
20660
21699
  var PRICE_OPERATIONS = [
20661
21700
  "activate",
@@ -20671,7 +21710,7 @@ function parseChoice4(value, allowed, flag) {
20671
21710
  }
20672
21711
  return match;
20673
21712
  }
20674
- function parsePositiveInt11(value, flag) {
21713
+ function parsePositiveInt12(value, flag) {
20675
21714
  const n = Number.parseInt(value, 10);
20676
21715
  if (!Number.isInteger(n) || n <= 0) {
20677
21716
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
@@ -20686,7 +21725,7 @@ function parseBool3(value, flag) {
20686
21725
  }
20687
21726
  function parseJsonFileOrInline6(value, flag) {
20688
21727
  const trimmed = value.trim();
20689
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync15(value, "utf8");
21728
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync17(value, "utf8");
20690
21729
  try {
20691
21730
  return JSON.parse(raw);
20692
21731
  } catch (error) {
@@ -20705,27 +21744,29 @@ function buildPriceInput(options) {
20705
21744
  throw new Error("Pass either --sale-ends-at or --sale-days, not both.");
20706
21745
  }
20707
21746
  const saleEndsAt = options.saleDays ? new Date(
20708
- Date.now() + parsePositiveInt11(options.saleDays, "--sale-days") * 864e5
21747
+ Date.now() + parsePositiveInt12(options.saleDays, "--sale-days") * 864e5
20709
21748
  ).toISOString() : options.saleEndsAt;
20710
21749
  return {
20711
21750
  type,
20712
- amountCents: parsePositiveInt11(options.amount, "--amount"),
21751
+ amountCents: parsePositiveInt12(options.amount, "--amount"),
20713
21752
  currency: parseChoice4(options.currency.toLowerCase(), PRICE_CURRENCIES, "--currency"),
20714
21753
  interval: options.interval ? parseChoice4(options.interval, PRICE_INTERVALS, "--interval") : void 0,
20715
- intervalCount: options.intervalCount ? parsePositiveInt11(options.intervalCount, "--interval-count") : void 0,
20716
- numberOfCycles: options.cycles ? parsePositiveInt11(options.cycles, "--cycles") : void 0,
21754
+ intervalCount: options.intervalCount ? parsePositiveInt12(options.intervalCount, "--interval-count") : void 0,
21755
+ numberOfCycles: options.cycles ? parsePositiveInt12(options.cycles, "--cycles") : void 0,
20717
21756
  installmentsAllowCancellation: options.allowCancellation ? true : void 0,
20718
- trialPeriodDays: options.trialDays ? parsePositiveInt11(options.trialDays, "--trial-days") : void 0,
21757
+ trialPeriodDays: options.trialDays ? parsePositiveInt12(options.trialDays, "--trial-days") : void 0,
20719
21758
  label: options.label,
20720
21759
  customInvoiceName: options.invoiceName,
20721
21760
  hidden: options.hidden ? true : void 0,
20722
21761
  description: options.description,
21762
+ saleStartsAt: options.saleStartsAt,
21763
+ saleStartDisplay: options.saleStartDisplay ? parseChoice4(options.saleStartDisplay, SALE_START_DISPLAYS, "--sale-start-display") : void 0,
20723
21764
  saleEndsAt,
20724
- quantityLimit: options.quantityLimit ? parsePositiveInt11(options.quantityLimit, "--quantity-limit") : void 0,
21765
+ quantityLimit: options.quantityLimit ? parsePositiveInt12(options.quantityLimit, "--quantity-limit") : void 0,
20725
21766
  showQuantityAvailable: options.showAvailable ? true : void 0,
20726
- compareAtAmountCents: options.compareAt ? parsePositiveInt11(options.compareAt, "--compare-at") : void 0,
21767
+ compareAtAmountCents: options.compareAt ? parsePositiveInt12(options.compareAt, "--compare-at") : void 0,
20727
21768
  accessInterval: options.accessInterval ? parseChoice4(options.accessInterval, PRICE_INTERVALS, "--access-interval") : void 0,
20728
- accessIntervalCount: options.accessCount ? parsePositiveInt11(options.accessCount, "--access-count") : void 0,
21769
+ accessIntervalCount: options.accessCount ? parsePositiveInt12(options.accessCount, "--access-count") : void 0,
20729
21770
  accessUntil: options.accessUntil || void 0,
20730
21771
  planId: options.plan
20731
21772
  };
@@ -20784,7 +21825,7 @@ function buildOfferUpdateInput(options) {
20784
21825
  set("language", options.language === void 0 ? void 0 : orNull4(options.language));
20785
21826
  set(
20786
21827
  "checkoutMediaWidth",
20787
- options.checkoutMediaWidth === void 0 ? void 0 : options.checkoutMediaWidth.trim().toLowerCase() === "none" ? null : parsePositiveInt11(options.checkoutMediaWidth, "--checkout-media-width")
21828
+ options.checkoutMediaWidth === void 0 ? void 0 : options.checkoutMediaWidth.trim().toLowerCase() === "none" ? null : parsePositiveInt12(options.checkoutMediaWidth, "--checkout-media-width")
20788
21829
  );
20789
21830
  set("showTestimonials", bool(options.showTestimonials, "--show-testimonials"));
20790
21831
  set("showPlanIncludes", bool(options.showPlanIncludes, "--show-plan-includes"));
@@ -20848,7 +21889,7 @@ function buildOfferUpdateInput(options) {
20848
21889
  set("enableClientToBuyMultipleTimes", bool(options.multiplePurchases, "--multiple-purchases"));
20849
21890
  set(
20850
21891
  "clientMultiplePurchaseLimit",
20851
- options.multiplePurchaseLimit === void 0 ? void 0 : options.multiplePurchaseLimit.trim().toLowerCase() === "none" ? null : parsePositiveInt11(options.multiplePurchaseLimit, "--multiple-purchase-limit")
21892
+ options.multiplePurchaseLimit === void 0 ? void 0 : options.multiplePurchaseLimit.trim().toLowerCase() === "none" ? null : parsePositiveInt12(options.multiplePurchaseLimit, "--multiple-purchase-limit")
20852
21893
  );
20853
21894
  set("allowUpgradeSubscriptionPlan", bool(options.allowPlanUpgrade, "--allow-plan-upgrade"));
20854
21895
  set("allowDowngradeSubscriptionPlan", bool(options.allowPlanDowngrade, "--allow-plan-downgrade"));
@@ -20980,10 +22021,20 @@ Prices (${checkout.prices.length}):
20980
22021
  amount: formatAmount2(p.amountCents, p.currency),
20981
22022
  active: yesNo(p.active),
20982
22023
  hidden: yesNo(p.hidden),
22024
+ saleStartsAt: p.saleStartsAt ?? "",
20983
22025
  saleEndsAt: p.saleEndsAt ?? "",
20984
22026
  quantityLimit: p.quantityLimit ?? ""
20985
22027
  })),
20986
- ["id", "label", "amount", "active", "hidden", "saleEndsAt", "quantityLimit"]
22028
+ [
22029
+ "id",
22030
+ "label",
22031
+ "amount",
22032
+ "active",
22033
+ "hidden",
22034
+ "saleStartsAt",
22035
+ "saleEndsAt",
22036
+ "quantityLimit"
22037
+ ]
20987
22038
  );
20988
22039
  process.stdout.write(`
20989
22040
  Order bumps (${checkout.orderBumps.length}):
@@ -21183,7 +22234,13 @@ Order bumps (${checkout.orderBumps.length}):
21183
22234
  price.command("add").description("Add a price variant to an offer").argument("<checkoutId>", "offer (checkout) id").requiredOption("--type <type>", "one_time | recurring | installments").requiredOption(
21184
22235
  "--amount <minor-units>",
21185
22236
  "unit price in minor units, e.g. 1999 = 19.99 (installments: each payment)"
21186
- ).requiredOption("--currency <code>", "pln | usd | eur | gbp").option("--interval <interval>", "billing interval: day | week | month | year").option("--interval-count <n>", "bill every n intervals").option("--cycles <n>", "installments: total number of payments (>= 2)").option("--allow-cancellation", "installments: let the buyer cancel the schedule").option("--trial-days <n>", "recurring: free-trial days before the first charge").option("--label <text>", "display label").option("--invoice-name <text>", "custom invoice line name").option("--hidden", "hide from the public price list (buyable via direct link)").option("--description <text>", "markdown description rendered under the price").option("--sale-ends-at <ISO>", "time-limited sale deadline (ISO 8601, in the future)").option("--sale-days <n>", "convenience: end the sale n days from now").option("--quantity-limit <n>", "seat limit").option("--show-available", "show the remaining quantity publicly").option("--compare-at <minor-units>", 'promo strike-through "was" price in minor units').option("--access-interval <interval>", "one-time: limited access window unit").option("--access-count <n>", "one-time: limited access window length").option(
22237
+ ).requiredOption("--currency <code>", "pln | usd | eur | gbp").option("--interval <interval>", "billing interval: day | week | month | year").option("--interval-count <n>", "bill every n intervals").option("--cycles <n>", "installments: total number of payments (>= 2)").option("--allow-cancellation", "installments: let the buyer cancel the schedule").option("--trial-days <n>", "recurring: free-trial days before the first charge").option("--label <text>", "display label").option("--invoice-name <text>", "custom invoice line name").option("--hidden", "hide from the public price list (buyable via direct link)").option("--description <text>", "markdown description rendered under the price").option(
22238
+ "--sale-starts-at <ISO>",
22239
+ "sale start (ISO 8601): the price stays hidden and unbuyable until then"
22240
+ ).option(
22241
+ "--sale-start-display <mode>",
22242
+ "before the start: hidden (default) | countdown (disabled tile with a countdown); needs --sale-starts-at"
22243
+ ).option("--sale-ends-at <ISO>", "time-limited sale deadline (ISO 8601, in the future)").option("--sale-days <n>", "convenience: end the sale n days from now").option("--quantity-limit <n>", "seat limit").option("--show-available", "show the remaining quantity publicly").option("--compare-at <minor-units>", 'promo strike-through "was" price in minor units').option("--access-interval <interval>", "one-time: limited access window unit").option("--access-count <n>", "one-time: limited access window length").option(
21187
22244
  "--access-until <iso>",
21188
22245
  "one-time: fixed access end for every buyer (ISO 8601, future; instead of --access-interval)"
21189
22246
  ).option("--plan <planId>", "link the price to a plan").action(async (checkoutId, options, command) => {
@@ -21208,7 +22265,7 @@ Order bumps (${checkout.orderBumps.length}):
21208
22265
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21209
22266
  const result = await client.updateCheckoutPrice(checkoutId, priceId, {
21210
22267
  operation,
21211
- newAmountCents: operation === "change-amount" && options.amount ? parsePositiveInt11(options.amount, "--amount") : void 0
22268
+ newAmountCents: operation === "change-amount" && options.amount ? parsePositiveInt12(options.amount, "--amount") : void 0
21212
22269
  });
21213
22270
  emit(flags, result, () => {
21214
22271
  process.stdout.write(
@@ -21259,7 +22316,7 @@ function assertOrderStatus(status) {
21259
22316
  }
21260
22317
  return status;
21261
22318
  }
21262
- function parsePositiveInt12(value, flag) {
22319
+ function parsePositiveInt13(value, flag) {
21263
22320
  const n = Number.parseInt(value, 10);
21264
22321
  if (!Number.isInteger(n) || n <= 0) {
21265
22322
  throw new Error(`Invalid ${flag}: ${value}`);
@@ -21269,7 +22326,7 @@ function parsePositiveInt12(value, flag) {
21269
22326
  function formatAmount3(amountCents, currency) {
21270
22327
  return `${(amountCents / 100).toFixed(2)} ${currency}`;
21271
22328
  }
21272
- function requireConfirm7(confirm, what) {
22329
+ function requireConfirm8(confirm, what) {
21273
22330
  if (!confirm) {
21274
22331
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
21275
22332
  }
@@ -21290,7 +22347,7 @@ function registerOrdersCommands(program2) {
21290
22347
  to: options.to,
21291
22348
  search: options.search,
21292
22349
  includeTest: options.includeTest,
21293
- limit: options.limit !== void 0 ? parsePositiveInt12(options.limit, "--limit") : void 0,
22350
+ limit: options.limit !== void 0 ? parsePositiveInt13(options.limit, "--limit") : void 0,
21294
22351
  cursor: options.cursor
21295
22352
  });
21296
22353
  emit(flags, page, () => {
@@ -21368,7 +22425,7 @@ Next page: --cursor ${page.nextCursor}
21368
22425
  ).argument("<orderId>", "order id (a `pending` order from `orders list --status pending`)").option("--issue-invoice", "also issue an invoice for the order (never issued without this)").option("--confirm", "required \u2014 acknowledge that access is granted and the buyer is emailed").action(
21369
22426
  async (orderId, options, command) => {
21370
22427
  const flags = getGlobalFlags(command);
21371
- requireConfirm7(options.confirm, `approve order ${orderId} (access + purchase email)`);
22428
+ requireConfirm8(options.confirm, `approve order ${orderId} (access + purchase email)`);
21372
22429
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21373
22430
  const result = await client.approveManualOrder(orderId, {
21374
22431
  issueInvoice: options.issueInvoice === true ? true : void 0
@@ -21385,7 +22442,7 @@ Next page: --cursor ${page.nextCursor}
21385
22442
  "Reject a manual (wire-transfer) order \u2014 cancels it and notifies the buyer. 409 = the order was already decided. Requires --confirm"
21386
22443
  ).argument("<orderId>", "order id (a `pending` order from `orders list --status pending`)").option("--confirm", "required \u2014 acknowledge that the order is cancelled and the buyer emailed").action(async (orderId, options, command) => {
21387
22444
  const flags = getGlobalFlags(command);
21388
- requireConfirm7(options.confirm, `reject order ${orderId} (cancel + notify the buyer)`);
22445
+ requireConfirm8(options.confirm, `reject order ${orderId} (cancel + notify the buyer)`);
21389
22446
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21390
22447
  const result = await client.rejectManualOrder(orderId);
21391
22448
  emit(flags, result, () => {
@@ -21414,7 +22471,7 @@ Next page: --cursor ${page.nextCursor}
21414
22471
  status: options.status !== void 0 ? assertOrderStatus(options.status) : void 0,
21415
22472
  search: options.search,
21416
22473
  includeTest: options.includeTest,
21417
- maxRows: options.maxRows !== void 0 ? parsePositiveInt12(options.maxRows, "--max-rows") : void 0
22474
+ maxRows: options.maxRows !== void 0 ? parsePositiveInt13(options.maxRows, "--max-rows") : void 0
21418
22475
  });
21419
22476
  writeFileSync5(options.out, result.csv, "utf8");
21420
22477
  emit(
@@ -21435,12 +22492,12 @@ Next page: --cursor ${page.nextCursor}
21435
22492
  }
21436
22493
 
21437
22494
  // src/commands/oto/index.ts
21438
- import { readFileSync as readFileSync16 } from "fs";
22495
+ import { readFileSync as readFileSync18 } from "fs";
21439
22496
  var MIN_TIMER_MINUTES = 1;
21440
22497
  var MAX_TIMER_MINUTES = 120;
21441
22498
  function parseJsonFileOrInline7(value, flag) {
21442
22499
  const trimmed = value.trim();
21443
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync16(value, "utf8");
22500
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync18(value, "utf8");
21444
22501
  try {
21445
22502
  return JSON.parse(raw);
21446
22503
  } catch (error) {
@@ -21628,9 +22685,9 @@ function registerOtoCommands(program2) {
21628
22685
  }
21629
22686
 
21630
22687
  // src/commands/pages/index.ts
21631
- import { existsSync as existsSync3, readFileSync as readFileSync17 } from "fs";
22688
+ import { existsSync as existsSync3, readFileSync as readFileSync19 } from "fs";
21632
22689
  import { readFile as readFile2 } from "fs/promises";
21633
- async function resolveHtml(options) {
22690
+ async function resolveHtml2(options) {
21634
22691
  if (options.file) {
21635
22692
  return readFile2(options.file, "utf8");
21636
22693
  }
@@ -21642,6 +22699,11 @@ var PAGE_INTENTS = [
21642
22699
  "sales-page",
21643
22700
  "thank-you",
21644
22701
  "waitlist",
22702
+ "newsletter",
22703
+ "challenge",
22704
+ "application",
22705
+ "upsell",
22706
+ "low-ticket",
21645
22707
  "generic"
21646
22708
  ];
21647
22709
  var SOURCE_SITE_MODES = ["rebuild", "inspired"];
@@ -21655,7 +22717,7 @@ function parseChoice5(value, allowed, flag) {
21655
22717
  }
21656
22718
  return match;
21657
22719
  }
21658
- function parsePositiveInt13(value, flag) {
22720
+ function parsePositiveInt14(value, flag) {
21659
22721
  const n = Number.parseInt(value, 10);
21660
22722
  if (!Number.isInteger(n) || n <= 0) {
21661
22723
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
@@ -21670,13 +22732,13 @@ function parseCsv4(value) {
21670
22732
  ];
21671
22733
  }
21672
22734
  function resolveTextOrFile2(value) {
21673
- const text = existsSync3(value) ? readFileSync17(value, "utf8") : value;
22735
+ const text = existsSync3(value) ? readFileSync19(value, "utf8") : value;
21674
22736
  if (!text.trim()) throw new Error("The text (or the file it points to) is empty.");
21675
22737
  return text;
21676
22738
  }
21677
22739
  function parseJsonObjectFileOrInline3(value, flag) {
21678
22740
  const trimmed = value.trim();
21679
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync17(value, "utf8");
22741
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync19(value, "utf8");
21680
22742
  let parsed;
21681
22743
  try {
21682
22744
  parsed = JSON.parse(raw);
@@ -21688,7 +22750,7 @@ function parseJsonObjectFileOrInline3(value, flag) {
21688
22750
  }
21689
22751
  return parsed;
21690
22752
  }
21691
- function requireConfirm8(confirm, what) {
22753
+ function requireConfirm9(confirm, what) {
21692
22754
  if (!confirm) {
21693
22755
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
21694
22756
  }
@@ -21725,7 +22787,7 @@ function buildGenerateInput(options) {
21725
22787
  if (options.eventStart !== void 0) input.eventStartAt = options.eventStart;
21726
22788
  if (options.eventTimezone !== void 0) input.eventTimezone = options.eventTimezone;
21727
22789
  if (options.eventDuration !== void 0) {
21728
- input.eventDurationMinutes = parsePositiveInt13(options.eventDuration, "--event-duration");
22790
+ input.eventDurationMinutes = parsePositiveInt14(options.eventDuration, "--event-duration");
21729
22791
  }
21730
22792
  if (options.eventMode !== void 0) {
21731
22793
  input.eventMode = parseChoice5(options.eventMode, EVENT_MODES2, "--event-mode");
@@ -21780,12 +22842,14 @@ function registerPagesCommands(program2) {
21780
22842
  process.stdout.write(` Source: ${page.source}
21781
22843
  `);
21782
22844
  process.stdout.write(` HTML: ${page.html.length} bytes
22845
+ `);
22846
+ process.stdout.write(` Cookie banner: ${page.hideCookieBanner ? "hidden" : "shown"}
21783
22847
  `);
21784
22848
  });
21785
22849
  });
21786
22850
  pages.command("create").description("Create a hosted page from an HTML file or inline HTML").requiredOption("--slug <slug>", "public URL slug (served at /page/<slug>)").requiredOption("--title <title>", "page title").option("--file <path>", "path to an .html file to host").option("--html <html>", "inline HTML body (alternative to --file)").option("--publish", "publish immediately instead of saving as draft").action(async (options, command) => {
21787
22851
  const flags = getGlobalFlags(command);
21788
- const html = await resolveHtml(options);
22852
+ const html = await resolveHtml2(options);
21789
22853
  if (!html) {
21790
22854
  throw new Error("Provide page HTML via --file <path> or --html <html>.");
21791
22855
  }
@@ -21802,13 +22866,17 @@ function registerPagesCommands(program2) {
21802
22866
  `);
21803
22867
  });
21804
22868
  });
21805
- pages.command("update").description("Update a page (slug, title, or HTML body)").argument("<id>", "page id").option("--slug <slug>", "new slug").option("--title <title>", "new title").option("--file <path>", "path to a new .html file").option("--html <html>", "new inline HTML body").action(async (id, options, command) => {
22869
+ pages.command("update").description("Update a page (slug, title, HTML body, or cookie-banner visibility)").argument("<id>", "page id").option("--slug <slug>", "new slug").option("--title <title>", "new title").option("--file <path>", "path to a new .html file").option("--html <html>", "new inline HTML body").option("--cookie-banner", "show the workspace cookie banner on this page again").option(
22870
+ "--no-cookie-banner",
22871
+ "hide the workspace cookie banner on this page (pixels still wait for consent given elsewhere)"
22872
+ ).action(async (id, options, command) => {
21806
22873
  const flags = getGlobalFlags(command);
21807
- const html = await resolveHtml(options);
22874
+ const html = await resolveHtml2(options);
21808
22875
  const input = {
21809
22876
  slug: options.slug,
21810
22877
  title: options.title,
21811
- html
22878
+ html,
22879
+ ...options.cookieBanner !== void 0 ? { hideCookieBanner: !options.cookieBanner } : {}
21812
22880
  };
21813
22881
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21814
22882
  const result = await client.updatePage(id, input);
@@ -21877,7 +22945,7 @@ function registerPagesCommands(program2) {
21877
22945
  "inline JSON object or .json path merged into the request body (explicit flags win)"
21878
22946
  ).option("--confirm", "required \u2014 acknowledge that AI credits will be consumed").action(async (options, command) => {
21879
22947
  const flags = getGlobalFlags(command);
21880
- requireConfirm8(options.confirm, "generate a page (consumes AI credits)");
22948
+ requireConfirm9(options.confirm, "generate a page (consumes AI credits)");
21881
22949
  const input = buildGenerateInput(options);
21882
22950
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21883
22951
  const result = await client.generatePage(input).catch(withSchedulerHint3);
@@ -21899,7 +22967,7 @@ function registerPagesCommands(program2) {
21899
22967
  ).action(
21900
22968
  async (pageId, options, command) => {
21901
22969
  const flags = getGlobalFlags(command);
21902
- requireConfirm8(
22970
+ requireConfirm9(
21903
22971
  options.confirm,
21904
22972
  "AI-edit the page (consumes AI credits, replaces the HTML)"
21905
22973
  );
@@ -21958,7 +23026,7 @@ function registerPagesCommands(program2) {
21958
23026
  ).argument("<pageId>", "page id").argument("<revisionId>", "revision id from `pages revisions`").option("--confirm", "required \u2014 acknowledge that the current HTML will be replaced").action(
21959
23027
  async (pageId, revisionId, options, command) => {
21960
23028
  const flags = getGlobalFlags(command);
21961
- requireConfirm8(options.confirm, "restore the revision (replaces the current HTML)");
23029
+ requireConfirm9(options.confirm, "restore the revision (replaces the current HTML)");
21962
23030
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21963
23031
  const result = await client.restorePageRevision(pageId, revisionId);
21964
23032
  emit(flags, result, () => {
@@ -21971,7 +23039,7 @@ function registerPagesCommands(program2) {
21971
23039
  "One-click activation of everything generated around a page: publishes the DRAFT delivery automation and schedules the webinar follow-up broadcasts at their stored send times. Emails will go out for real; items whose copy still carries [FILL IN] placeholders are skipped, never sent. Requires --confirm."
21972
23040
  ).argument("<pageId>", "page id").option("--confirm", "required \u2014 acknowledge that automations go live and emails will send").action(async (pageId, options, command) => {
21973
23041
  const flags = getGlobalFlags(command);
21974
- requireConfirm8(options.confirm, "activate the page automations (emails will send)");
23042
+ requireConfirm9(options.confirm, "activate the page automations (emails will send)");
21975
23043
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21976
23044
  const result = await client.activatePageAutomations(pageId);
21977
23045
  emit(flags, result, () => {
@@ -22001,7 +23069,7 @@ function registerPagesCommands(program2) {
22001
23069
  }
22002
23070
 
22003
23071
  // src/commands/products/index.ts
22004
- import { readFileSync as readFileSync18 } from "fs";
23072
+ import { readFileSync as readFileSync20 } from "fs";
22005
23073
  var KB_TREE_CHANGED_ERROR = "tree-changed";
22006
23074
  var JSON_FLAG_HINT3 = "(write it as --json=<value> \u2014 a bare --json is the global JSON-output flag)";
22007
23075
  function parseCoverRef(value) {
@@ -22107,7 +23175,7 @@ function assertOneOf3(value, allowed, flag) {
22107
23175
  }
22108
23176
  function parseJsonFileOrInline8(value, flag) {
22109
23177
  const trimmed = value.trim();
22110
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync18(value, "utf8");
23178
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync20(value, "utf8");
22111
23179
  try {
22112
23180
  return JSON.parse(raw);
22113
23181
  } catch (error) {
@@ -22702,7 +23770,7 @@ function registerProductsCommands(program2) {
22702
23770
  if (options.content !== void 0 && options.contentFile !== void 0) {
22703
23771
  throw new Error("Use either --content or --content-file, not both.");
22704
23772
  }
22705
- const editorContent = options.contentFile !== void 0 ? readFileSync18(options.contentFile, "utf8") : options.content;
23773
+ const editorContent = options.contentFile !== void 0 ? readFileSync20(options.contentFile, "utf8") : options.content;
22706
23774
  if (editorContent === void 0) {
22707
23775
  throw new Error("Pass the description via --content or --content-file.");
22708
23776
  }
@@ -22801,7 +23869,7 @@ function registerProductsCommands(program2) {
22801
23869
  if (options.content !== void 0 && options.contentFile !== void 0) {
22802
23870
  throw new Error("Use either --content or --content-file, not both.");
22803
23871
  }
22804
- return options.contentFile !== void 0 ? readFileSync18(options.contentFile, "utf8") : options.content;
23872
+ return options.contentFile !== void 0 ? readFileSync20(options.contentFile, "utf8") : options.content;
22805
23873
  };
22806
23874
  kb.command("get").description(
22807
23875
  "Show the knowledge base: categories (tree keys = the --category ids) + all articles (drafts and unavailable included \u2014 the creator view)"
@@ -22966,15 +24034,25 @@ function registerProductsCommands(program2) {
22966
24034
  }
22967
24035
  );
22968
24036
  const kbCategory = kb.command("category").description("Manage knowledge-base categories (add, rename, delete)");
22969
- kbCategory.command("add").description("Add a root category to the knowledge base").argument("<productId>", "knowledge-base product id").requiredOption("--name <name>", "category display name").action(async (productId, options, command) => {
22970
- const flags = getGlobalFlags(command);
22971
- const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22972
- const result = await client.addKbCategory(productId, options.name);
22973
- emit(flags, result, () => {
22974
- process.stdout.write(`Added category ${result.categoryId} on product ${productId}.
22975
- `);
22976
- });
22977
- });
24037
+ kbCategory.command("add").description(
24038
+ "Add a category to the knowledge base (a root category, or a subcategory with --parent)"
24039
+ ).argument("<productId>", "knowledge-base product id").requiredOption("--name <name>", "category display name").option(
24040
+ "--parent <categoryId>",
24041
+ "nest the new category under this existing category (tree key from `products kb get`); nesting depth is unlimited"
24042
+ ).action(
24043
+ async (productId, options, command) => {
24044
+ const flags = getGlobalFlags(command);
24045
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24046
+ const result = await client.addKbCategory(productId, options.name, options.parent);
24047
+ emit(flags, result, () => {
24048
+ process.stdout.write(
24049
+ options.parent ? `Added subcategory ${result.categoryId} under ${options.parent} on product ${productId}.
24050
+ ` : `Added category ${result.categoryId} on product ${productId}.
24051
+ `
24052
+ );
24053
+ });
24054
+ }
24055
+ );
22978
24056
  kbCategory.command("rename").description("Rename a category (its articles' denormalized category name follows)").argument("<productId>", "knowledge-base product id").argument("<categoryId>", "category id (tree key from `products kb get`)").requiredOption("--name <name>", "the new category name").action(
22979
24057
  async (productId, categoryId, options, command) => {
22980
24058
  const flags = getGlobalFlags(command);
@@ -23011,7 +24089,7 @@ function registerProductsCommands(program2) {
23011
24089
  }
23012
24090
 
23013
24091
  // src/commands/referrals/index.ts
23014
- import { readFileSync as readFileSync19 } from "fs";
24092
+ import { readFileSync as readFileSync21 } from "fs";
23015
24093
  var PROGRAM_KINDS = ["referral", "affiliate"];
23016
24094
  var WRITABLE_STATUSES = ["active", "inactive"];
23017
24095
  var ENROLLMENT_MODES = ["open", "inviteOnly", "closed"];
@@ -23036,7 +24114,7 @@ function parseIdList3(value, flag) {
23036
24114
  }
23037
24115
  function parseJsonObjectFileOrInline4(value, flag) {
23038
24116
  const trimmed = value.trim();
23039
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync19(value, "utf8");
24117
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync21(value, "utf8");
23040
24118
  let parsed;
23041
24119
  try {
23042
24120
  parsed = JSON.parse(raw);
@@ -23074,7 +24152,7 @@ function parseDeepConfig(value) {
23074
24152
  }
23075
24153
  return parsed;
23076
24154
  }
23077
- function requireConfirm9(confirm, what) {
24155
+ function requireConfirm10(confirm, what) {
23078
24156
  if (!confirm) {
23079
24157
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
23080
24158
  }
@@ -23309,7 +24387,7 @@ Failed deliveries (${result.failedDeliveries.length})
23309
24387
  "Archive a program \u2014 new referrals stop counting and it leaves the active list. Requires --confirm."
23310
24388
  ).argument("<programId>", "program id").option("--confirm", "required \u2014 acknowledge the archive").action(async (programId, options, command) => {
23311
24389
  const flags = getGlobalFlags(command);
23312
- requireConfirm9(options.confirm, `archive program ${programId}`);
24390
+ requireConfirm10(options.confirm, `archive program ${programId}`);
23313
24391
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23314
24392
  const result = await client.archiveReferralProgram(programId);
23315
24393
  emit(flags, result, () => {
@@ -23341,7 +24419,7 @@ Failed deliveries (${result.failedDeliveries.length})
23341
24419
  "Enroll EVERY client with access to one of the program products. Requires --confirm. Reports enrolled / alreadyEnrolled / skippedWithoutAccount counts."
23342
24420
  ).argument("<programId>", "program id").option("--confirm", "required \u2014 acknowledge the bulk enrollment").action(async (programId, options, command) => {
23343
24421
  const flags = getGlobalFlags(command);
23344
- requireConfirm9(options.confirm, `enroll all clients in program ${programId}`);
24422
+ requireConfirm10(options.confirm, `enroll all clients in program ${programId}`);
23345
24423
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23346
24424
  const result = await client.enrollAllReferralClients(programId);
23347
24425
  emit(flags, result, () => {
@@ -23357,7 +24435,7 @@ Failed deliveries (${result.failedDeliveries.length})
23357
24435
  async (programId, entryId, options, command) => {
23358
24436
  const flags = getGlobalFlags(command);
23359
24437
  const decision = parseChoice6(options.decision, REWARD_DECISIONS, "--decision");
23360
- requireConfirm9(options.confirm, `${decision} reward ${entryId} in program ${programId}`);
24438
+ requireConfirm10(options.confirm, `${decision} reward ${entryId} in program ${programId}`);
23361
24439
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23362
24440
  const result = await client.resolveReferralReward(programId, entryId, { decision });
23363
24441
  emit(flags, result, () => {
@@ -23387,8 +24465,8 @@ Failed deliveries (${result.failedDeliveries.length})
23387
24465
  }
23388
24466
 
23389
24467
  // src/commands/segments/index.ts
23390
- import { readFileSync as readFileSync20 } from "fs";
23391
- var SUBSCRIPTION_STATUSES = ["pending", "subscribed", "unsubscribed"];
24468
+ import { readFileSync as readFileSync22 } from "fs";
24469
+ var SUBSCRIPTION_STATUSES2 = ["pending", "subscribed", "unsubscribed"];
23392
24470
  function collectRepeatable6(value, previous) {
23393
24471
  return [...previous, value];
23394
24472
  }
@@ -23399,14 +24477,14 @@ function parseMinorUnits(value, flag) {
23399
24477
  }
23400
24478
  return n;
23401
24479
  }
23402
- var CONDITION_FLAGS_HELP = "--has-tag/--without-tag, --owns/--not-owns, --spent-gte/--spent-lte, --subscription, or --json-file";
24480
+ var CONDITION_FLAGS_HELP = "--has-tag/--without-tag, --owns/--not-owns, --owns-direct/--not-owns-direct, --spent-gte/--spent-lte, --subscription, or --json-file";
23403
24481
  function resolveConditions(options) {
23404
- const hasFlagConditions = options.hasTag.length > 0 || options.withoutTag.length > 0 || options.owns.length > 0 || options.notOwns.length > 0 || options.spentGte !== void 0 || options.spentLte !== void 0 || options.subscription !== void 0;
24482
+ const hasFlagConditions = options.hasTag.length > 0 || options.withoutTag.length > 0 || options.owns.length > 0 || options.notOwns.length > 0 || options.ownsDirect.length > 0 || options.notOwnsDirect.length > 0 || options.spentGte !== void 0 || options.spentLte !== void 0 || options.subscription !== void 0;
23405
24483
  if (options.jsonFile !== void 0) {
23406
24484
  if (hasFlagConditions) {
23407
24485
  throw new Error("Use either condition flags or --json-file, not both.");
23408
24486
  }
23409
- const raw = readFileSync20(options.jsonFile, "utf8");
24487
+ const raw = readFileSync22(options.jsonFile, "utf8");
23410
24488
  let parsed;
23411
24489
  try {
23412
24490
  parsed = JSON.parse(raw);
@@ -23434,6 +24512,12 @@ function resolveConditions(options) {
23434
24512
  for (const productId of options.notOwns) {
23435
24513
  conditions.push({ field: "product", operator: "not-owns", productId });
23436
24514
  }
24515
+ for (const productId of options.ownsDirect) {
24516
+ conditions.push({ field: "product", operator: "owns", productId, accessScope: "direct" });
24517
+ }
24518
+ for (const productId of options.notOwnsDirect) {
24519
+ conditions.push({ field: "product", operator: "not-owns", productId, accessScope: "direct" });
24520
+ }
23437
24521
  if (options.spentGte !== void 0) {
23438
24522
  conditions.push({
23439
24523
  field: "totalSpent",
@@ -23449,10 +24533,10 @@ function resolveConditions(options) {
23449
24533
  });
23450
24534
  }
23451
24535
  if (options.subscription !== void 0) {
23452
- const status = SUBSCRIPTION_STATUSES.find((s) => s === options.subscription);
24536
+ const status = SUBSCRIPTION_STATUSES2.find((s) => s === options.subscription);
23453
24537
  if (!status) {
23454
24538
  throw new Error(
23455
- `Invalid --subscription: ${options.subscription} (expected one of ${SUBSCRIPTION_STATUSES.join(", ")})`
24539
+ `Invalid --subscription: ${options.subscription} (expected one of ${SUBSCRIPTION_STATUSES2.join(", ")})`
23456
24540
  );
23457
24541
  }
23458
24542
  conditions.push({ field: "emailSubscription", operator: "is", status });
@@ -23472,12 +24556,22 @@ function withConditionOptions(command) {
23472
24556
  []
23473
24557
  ).option(
23474
24558
  "--owns <productId>",
23475
- "client has an ACTIVE access grant for this product (repeatable)",
24559
+ "client has ACTIVE access to this product - a direct grant OR through a bundle (repeatable)",
23476
24560
  collectRepeatable6,
23477
24561
  []
23478
24562
  ).option(
23479
24563
  "--not-owns <productId>",
23480
- "client has NO active access grant for this product (repeatable)",
24564
+ "client has NO active access to this product, direct or through a bundle (repeatable)",
24565
+ collectRepeatable6,
24566
+ []
24567
+ ).option(
24568
+ "--owns-direct <productId>",
24569
+ "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)",
24570
+ collectRepeatable6,
24571
+ []
24572
+ ).option(
24573
+ "--not-owns-direct <productId>",
24574
+ "client has NO active direct grant on this product - bundle holders DO match (repeatable)",
23481
24575
  collectRepeatable6,
23482
24576
  []
23483
24577
  ).option(
@@ -23485,7 +24579,7 @@ function withConditionOptions(command) {
23485
24579
  "lifetime spend at least this (minor units, e.g. 10000 = 100.00)"
23486
24580
  ).option("--spent-lte <minorUnits>", "lifetime spend at most this (minor units)").option(
23487
24581
  "--subscription <status>",
23488
- `marketing-email subscription state is (${SUBSCRIPTION_STATUSES.join(" | ")})`
24582
+ `marketing-email subscription state is (${SUBSCRIPTION_STATUSES2.join(" | ")})`
23489
24583
  ).option(
23490
24584
  "--json-file <path>",
23491
24585
  "read the conditions as a JSON array from a file instead of flags"
@@ -23496,7 +24590,7 @@ function describeCondition(condition) {
23496
24590
  case "tag":
23497
24591
  return `${condition.operator === "has" ? "has" : "lacks"} tag ${condition.tagId}`;
23498
24592
  case "product":
23499
- return `${condition.operator === "owns" ? "owns" : "does not own"} product ${condition.productId}`;
24593
+ return `${condition.operator === "owns" ? "owns" : "does not own"} product ${condition.productId}${condition.accessScope === "direct" ? " (direct)" : ""}`;
23500
24594
  case "totalSpent":
23501
24595
  return `total spent ${condition.operator === "gte" ? ">=" : "<="} ${condition.amount} (minor units)`;
23502
24596
  case "emailSubscription":
@@ -23505,6 +24599,10 @@ function describeCondition(condition) {
23505
24599
  return `last email engagement ${condition.operator === "within" ? "within" : "not within"} the last ${condition.days} days`;
23506
24600
  case "lastOrderAt":
23507
24601
  return `last paid order ${condition.operator === "within" ? "within" : "not within"} the last ${condition.days} days`;
24602
+ case "smsSubscription":
24603
+ return `sms consent is ${condition.status}`;
24604
+ case "phoneNumber":
24605
+ return condition.operator === "has" ? "has a phone number" : "has no phone number";
23508
24606
  }
23509
24607
  }
23510
24608
  function registerSegmentsCommands(program2) {
@@ -23602,7 +24700,7 @@ function registerSegmentsCommands(program2) {
23602
24700
  }
23603
24701
 
23604
24702
  // src/commands/site/index.ts
23605
- import { existsSync as existsSync4, readFileSync as readFileSync21 } from "fs";
24703
+ import { existsSync as existsSync4, readFileSync as readFileSync23 } from "fs";
23606
24704
  var MAIN_PAGE_MODES = {
23607
24705
  sales: "/",
23608
24706
  dashboard: "/dashboard",
@@ -23626,14 +24724,14 @@ var HTTP_CONFLICT = 409;
23626
24724
  var HTTP_BAD_REQUEST = 400;
23627
24725
  var HTTP_NOT_FOUND = 404;
23628
24726
  var HTTP_FORBIDDEN = 403;
23629
- function requireConfirm10(confirm, what) {
24727
+ function requireConfirm11(confirm, what) {
23630
24728
  if (!confirm) {
23631
24729
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
23632
24730
  }
23633
24731
  }
23634
24732
  function parseJsonObjectFileOrInline5(value, flag) {
23635
24733
  const trimmed = value.trim();
23636
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync21(value, "utf8");
24734
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync23(value, "utf8");
23637
24735
  let parsed;
23638
24736
  try {
23639
24737
  parsed = JSON.parse(raw);
@@ -23685,7 +24783,7 @@ function parseDomainList(value, what) {
23685
24783
  return items;
23686
24784
  }
23687
24785
  function readMarkdownOrFile(value) {
23688
- return existsSync4(value) ? readFileSync21(value, "utf8") : value;
24786
+ return existsSync4(value) ? readFileSync23(value, "utf8") : value;
23689
24787
  }
23690
24788
  function assertNonEmptyBody(body, hint) {
23691
24789
  if (Object.keys(body).length === 0) {
@@ -24168,7 +25266,7 @@ function registerSiteCommands(program2) {
24168
25266
  throw new Error("Pass exactly one of --on or --off.");
24169
25267
  }
24170
25268
  const active = Boolean(options.on);
24171
- requireConfirm10(options.confirm, active ? "publish the site" : "take the site offline");
25269
+ requireConfirm11(options.confirm, active ? "publish the site" : "take the site offline");
24172
25270
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24173
25271
  const result = await client.activateSite({ active });
24174
25272
  emit(
@@ -24183,7 +25281,7 @@ function registerSiteCommands(program2) {
24183
25281
  "Change the slug \u2014 heavy: re-registers Stripe/Firebase/Vercel hosts and purges caches. 409 = slug taken. Requires --confirm"
24184
25282
  ).argument("<slug>", 'new slug (lower-case letters, digits, dashes; "www" is refused)').option("--confirm", "required \u2014 acknowledge the URL change for every existing link").action(async (value, options, command) => {
24185
25283
  const flags = getGlobalFlags(command);
24186
- requireConfirm10(options.confirm, `change the site slug to "${value}"`);
25284
+ requireConfirm11(options.confirm, `change the site slug to "${value}"`);
24187
25285
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24188
25286
  const result = await client.updateSiteSlug({ slug: value.trim() }).catch(withSlugHint);
24189
25287
  emit(flags, result, () => process.stdout.write(`Slug changed to "${value.trim()}".
@@ -24195,7 +25293,7 @@ function registerSiteCommands(program2) {
24195
25293
  ).argument("<domains>", "comma-separated hostnames, e.g. shop.example.com,example.com \u2014 or none").option("--confirm", "required \u2014 acknowledge the domain list replace").action(async (value, options, command) => {
24196
25294
  const flags = getGlobalFlags(command);
24197
25295
  const list = value.trim().toLowerCase() === "none" ? [] : parseDomainList(value, "<domains>");
24198
- requireConfirm10(
25296
+ requireConfirm11(
24199
25297
  options.confirm,
24200
25298
  list.length === 0 ? "remove every custom domain" : `set the custom domains to ${list.join(", ")}`
24201
25299
  );
@@ -24280,7 +25378,7 @@ function registerSiteCommands(program2) {
24280
25378
  "Nothing to update \u2014 pass --terms-content|--terms-url and/or --privacy-content|--privacy-url."
24281
25379
  );
24282
25380
  }
24283
- requireConfirm10(options.confirm, "change the legal documents");
25381
+ requireConfirm11(options.confirm, "change the legal documents");
24284
25382
  const body = {};
24285
25383
  if (options.language !== void 0) body.language = options.language;
24286
25384
  if (terms) body.terms = terms;
@@ -24331,27 +25429,323 @@ function printStorefront(config) {
24331
25429
  );
24332
25430
  }
24333
25431
 
24334
- // src/commands/social/index.ts
24335
- var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
24336
- function composeInput(options) {
24337
- if (!options.text) {
24338
- throw new Error("Provide the post text via --text.");
25432
+ // src/commands/smsCampaigns/index.ts
25433
+ import { readFileSync as readFileSync24 } from "fs";
25434
+ var SMS_PROVIDERS = ["twilio", "smsapi"];
25435
+ var RECIPIENT_STATUSES2 = [
25436
+ "pending",
25437
+ "sent",
25438
+ "failed",
25439
+ "skipped-invalid-number"
25440
+ ];
25441
+ var SCHEDULER_UNAVAILABLE_STATUS4 = 503;
25442
+ function parsePositiveInt15(value, flag) {
25443
+ const n = Number.parseInt(value, 10);
25444
+ if (!Number.isInteger(n) || n <= 0) {
25445
+ throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
24339
25446
  }
24340
- const targets = splitList(options.targets);
24341
- if (targets.length === 0) {
24342
- throw new Error("Provide at least one target via --targets (see `social accounts`).");
25447
+ return n;
25448
+ }
25449
+ function requireConfirm12(confirm, refusal) {
25450
+ if (!confirm) throw new Error(refusal);
25451
+ }
25452
+ function withSchedulerHint4(error) {
25453
+ if (error instanceof ApiError && error.status === SCHEDULER_UNAVAILABLE_STATUS4) {
25454
+ throw new ApiError(
25455
+ error.status,
25456
+ "Trigger.dev scheduler not configured \u2014 SMS campaigns cannot be sent on this environment.",
25457
+ "The API needs TRIGGER_SECRET_KEY (Trigger.dev) set; ask the operator to configure it."
25458
+ );
24343
25459
  }
24344
- const mediaIds = splitList(options.media);
24345
- return {
24346
- text: options.text,
24347
- targets,
24348
- ...mediaIds.length > 0 ? { mediaIds } : {},
24349
- ...options.firstComment ? { firstComment: options.firstComment } : {},
24350
- ...options.at ? { scheduledAt: options.at } : {},
24351
- ...options.publish ? { publish: true } : {}
24352
- };
25460
+ throw error;
24353
25461
  }
24354
- function registerSocialCommands(program2) {
25462
+ function resolveBody(options) {
25463
+ if (options.body !== void 0 && options.bodyFile !== void 0) {
25464
+ throw new Error("Use either --body or --body-file, not both.");
25465
+ }
25466
+ return options.bodyFile !== void 0 ? readFileSync24(options.bodyFile, "utf8") : options.body;
25467
+ }
25468
+ function resolveAudience2(options) {
25469
+ if (options.segment !== void 0 && options.allSubscribed) {
25470
+ throw new Error("Use either --segment or --all-subscribed, not both.");
25471
+ }
25472
+ if (options.segment !== void 0) return { type: "segment", segmentId: options.segment };
25473
+ if (options.allSubscribed) return { type: "all-sms-subscribed" };
25474
+ return void 0;
25475
+ }
25476
+ function describeAudience2(audience) {
25477
+ return audience.type === "segment" ? `segment ${audience.segmentId} (SMS-subscribed clients only)` : "all SMS-subscribed clients";
25478
+ }
25479
+ function resolveProvider(options) {
25480
+ if (options.provider !== void 0 && options.defaultProvider) {
25481
+ throw new Error("Use either --provider or --default-provider, not both.");
25482
+ }
25483
+ if (options.defaultProvider) return null;
25484
+ if (options.provider === void 0) return void 0;
25485
+ const match = SMS_PROVIDERS.find((p) => p === options.provider);
25486
+ if (!match) {
25487
+ throw new Error(
25488
+ `Invalid --provider: ${options.provider} (expected one of ${SMS_PROVIDERS.join(", ")})`
25489
+ );
25490
+ }
25491
+ return match;
25492
+ }
25493
+ function resolveSender3(options) {
25494
+ if (options.sender !== void 0 && options.defaultSender) {
25495
+ throw new Error("Use either --sender or --default-sender, not both.");
25496
+ }
25497
+ return options.defaultSender ? null : options.sender;
25498
+ }
25499
+ function resolveOptOutHint(options) {
25500
+ if (options.optOutHint !== void 0 && options.clearOptOutHint) {
25501
+ throw new Error("Use either --opt-out-hint or --clear-opt-out-hint, not both.");
25502
+ }
25503
+ return options.clearOptOutHint ? null : options.optOutHint;
25504
+ }
25505
+ function printCampaignDetail2(campaign) {
25506
+ process.stdout.write(`SMS campaign ${campaign.id}
25507
+ `);
25508
+ process.stdout.write(` Name: ${campaign.name}
25509
+ `);
25510
+ process.stdout.write(` Status: ${campaign.status}
25511
+ `);
25512
+ process.stdout.write(` Provider: ${campaign.provider ?? "(workspace default)"}
25513
+ `);
25514
+ process.stdout.write(` Sender: ${campaign.sender ?? "(connection's default)"}
25515
+ `);
25516
+ process.stdout.write(` Audience: ${describeAudience2(campaign.audience)}
25517
+ `);
25518
+ process.stdout.write(` Recipients: ${campaign.recipientCount ?? "(not resolved yet)"}
25519
+ `);
25520
+ if (campaign.scheduledAt) process.stdout.write(` Scheduled at: ${campaign.scheduledAt}
25521
+ `);
25522
+ if (campaign.sendStartedAt) {
25523
+ process.stdout.write(` Send started at: ${campaign.sendStartedAt}
25524
+ `);
25525
+ }
25526
+ if (campaign.completedAt) process.stdout.write(` Completed at: ${campaign.completedAt}
25527
+ `);
25528
+ if (campaign.failureReason) process.stdout.write(` Failure: ${campaign.failureReason}
25529
+ `);
25530
+ const { stats } = campaign;
25531
+ process.stdout.write(
25532
+ ` Stats: sent ${stats.sent}, failed ${stats.failed}, skipped (invalid number) ${stats.skippedInvalidNumber}, billed parts ${stats.segments}
25533
+ `
25534
+ );
25535
+ process.stdout.write(` Opt-out line: ${campaign.optOutHint ?? "(none)"}
25536
+ `);
25537
+ process.stdout.write(` Body (${campaign.body.length} chars):
25538
+ `);
25539
+ for (const line of campaign.body.split("\n")) process.stdout.write(` ${line}
25540
+ `);
25541
+ }
25542
+ function registerSmsCampaignsCommands(program2) {
25543
+ const smsCampaigns = program2.command("sms-campaigns").description(
25544
+ "Manage SMS campaigns (one-off text broadcasts through the workspace's own Twilio / SMSAPI account) \u2014 drafts are harmless; `send` texts people and costs money"
25545
+ );
25546
+ smsCampaigns.command("list").description("List SMS campaigns").action(async (_, command) => {
25547
+ const flags = getGlobalFlags(command);
25548
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25549
+ const page = await client.listSmsCampaigns();
25550
+ emit(flags, page, () => {
25551
+ printTable(
25552
+ page.campaigns.map((c) => ({
25553
+ id: c.id,
25554
+ name: c.name,
25555
+ status: c.status,
25556
+ audience: describeAudience2(c.audience),
25557
+ recipients: c.recipientCount ?? "",
25558
+ sent: c.stats.sent,
25559
+ parts: c.stats.segments
25560
+ })),
25561
+ ["id", "name", "status", "audience", "recipients", "sent", "parts"]
25562
+ );
25563
+ });
25564
+ });
25565
+ smsCampaigns.command("get").description("Show a single SMS campaign (status, provider, sender, audience, stats, body)").argument("<id>", "SMS campaign id").action(async (id, _options, command) => {
25566
+ const flags = getGlobalFlags(command);
25567
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25568
+ const result = await client.getSmsCampaign(id);
25569
+ emit(flags, result, () => printCampaignDetail2(result.campaign));
25570
+ });
25571
+ smsCampaigns.command("create").description(
25572
+ "Create a DRAFT SMS campaign \u2014 nothing is sent; only --name is required at draft time (the body and a connected provider are enforced by `sms-campaigns send`)"
25573
+ ).requiredOption("--name <name>", "internal label shown in the campaigns list").option("--body <text>", "message text; {{firstName}} / {{lastName}} tokens are supported").option("--body-file <path>", "read the message text from a file").option("--opt-out-hint <text>", "opt-out line appended to every message (counts as text)").option("--provider <provider>", `send through this provider (${SMS_PROVIDERS.join(" | ")})`).option("--sender <name>", "sender name override (SMSAPI); omit for the connection's default").option("--segment <segmentId>", "send to a saved segment (SMS-subscribed clients in it only)").option("--all-subscribed", "send to every SMS-subscribed client (the default audience)").action(
25574
+ async (options, command) => {
25575
+ const flags = getGlobalFlags(command);
25576
+ const body = resolveBody(options);
25577
+ const audience = resolveAudience2(options);
25578
+ const provider = resolveProvider(options);
25579
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25580
+ const result = await client.createSmsCampaign({
25581
+ name: options.name,
25582
+ body,
25583
+ optOutHint: options.optOutHint,
25584
+ provider,
25585
+ sender: options.sender,
25586
+ audience
25587
+ });
25588
+ emit(flags, result, () => {
25589
+ process.stdout.write(
25590
+ `Created draft SMS campaign ${result.campaign.id} (${result.campaign.name}) \u2014 audience: ${describeAudience2(result.campaign.audience)}. Nothing is sent until \`sms-campaigns send\`.
25591
+ `
25592
+ );
25593
+ });
25594
+ }
25595
+ );
25596
+ smsCampaigns.command("update").description("Update a DRAFT SMS campaign (campaigns past draft are immutable except cancel)").argument("<id>", "SMS campaign id").option("--name <name>", "new internal label").option("--body <text>", "new message text (replaces the whole body)").option("--body-file <path>", "read the new message text from a file").option("--opt-out-hint <text>", "new opt-out line").option("--clear-opt-out-hint", "remove the opt-out line").option("--provider <provider>", `send through this provider (${SMS_PROVIDERS.join(" | ")})`).option("--default-provider", "go back to the workspace default provider").option("--sender <name>", "sender name override (SMSAPI)").option("--default-sender", "go back to the connection's default sender").option("--segment <segmentId>", "switch the audience to a saved segment").option("--all-subscribed", "switch the audience to every SMS-subscribed client").action(
25597
+ async (id, options, command) => {
25598
+ const flags = getGlobalFlags(command);
25599
+ const input = {};
25600
+ if (options.name !== void 0) input.name = options.name;
25601
+ const body = resolveBody(options);
25602
+ if (body !== void 0) input.body = body;
25603
+ const optOutHint = resolveOptOutHint(options);
25604
+ if (optOutHint !== void 0) input.optOutHint = optOutHint;
25605
+ const provider = resolveProvider(options);
25606
+ if (provider !== void 0) input.provider = provider;
25607
+ const sender = resolveSender3(options);
25608
+ if (sender !== void 0) input.sender = sender;
25609
+ const audience = resolveAudience2(options);
25610
+ if (audience !== void 0) input.audience = audience;
25611
+ if (Object.keys(input).length === 0) {
25612
+ throw new Error(
25613
+ "Nothing to update \u2014 pass at least one change flag (see `sms-campaigns update --help`)."
25614
+ );
25615
+ }
25616
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25617
+ const result = await client.updateSmsCampaign(id, input);
25618
+ emit(flags, result, () => {
25619
+ process.stdout.write(`Updated SMS campaign ${result.campaign.id} (still a draft).
25620
+ `);
25621
+ });
25622
+ }
25623
+ );
25624
+ smsCampaigns.command("delete").description(
25625
+ "PERMANENTLY delete an SMS campaign \u2014 cannot be undone; draft/cancelled/failed only (sent campaigns are audit records and stay). Requires --confirm"
25626
+ ).argument("<id>", "SMS campaign id").option("--confirm", "required \u2014 acknowledge the permanent deletion").action(async (id, options, command) => {
25627
+ const flags = getGlobalFlags(command);
25628
+ requireConfirm12(
25629
+ options.confirm,
25630
+ `Refusing to delete SMS campaign ${id} without --confirm. Re-run with --confirm to proceed.`
25631
+ );
25632
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25633
+ const result = await client.deleteSmsCampaign(id);
25634
+ emit(flags, result, () => {
25635
+ process.stdout.write(`Deleted SMS campaign ${id}.
25636
+ `);
25637
+ });
25638
+ });
25639
+ smsCampaigns.command("send").description(
25640
+ "\u26A0\uFE0F SENDS the campaign \u2014 REAL text messages to the entire resolved audience through the workspace's own SMS provider account: it COSTS MONEY per message part and is IRREVERSIBLE. `sms-campaigns cancel` only stops remaining sends. Requires --confirm. Omit --at to send NOW; --at schedules (max 30 days out)."
25641
+ ).argument("<id>", "SMS campaign id").option(
25642
+ "--at <isoTimestamp>",
25643
+ "schedule the send (ISO, max 30 days out) instead of sending now"
25644
+ ).option("--confirm", "required \u2014 acknowledge this texts the audience for real").action(async (id, options, command) => {
25645
+ const flags = getGlobalFlags(command);
25646
+ requireConfirm12(
25647
+ options.confirm,
25648
+ `Refusing to send without --confirm. \`sms-campaigns send\` texts the ENTIRE resolved audience for real through the workspace's SMS provider account, is billed per message part and cannot be undone. Re-run with --confirm to proceed (check the draft first with \`sms-campaigns get ${id}\`).`
25649
+ );
25650
+ if (options.at !== void 0 && Number.isNaN(Date.parse(options.at))) {
25651
+ throw new Error(
25652
+ `Invalid --at timestamp: ${options.at} (expected ISO, e.g. 2026-10-15T09:00:00Z)`
25653
+ );
25654
+ }
25655
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25656
+ const result = await client.sendSmsCampaign(id, options.at).catch(withSchedulerHint4);
25657
+ emit(flags, result, () => {
25658
+ const { campaign } = result;
25659
+ if (campaign.status === "scheduled") {
25660
+ process.stdout.write(
25661
+ `SMS campaign ${campaign.id} scheduled for ${campaign.scheduledAt} \u2014 stop it before then with \`sms-campaigns cancel\`.
25662
+ `
25663
+ );
25664
+ } else {
25665
+ process.stdout.write(
25666
+ `SMS campaign ${campaign.id} is sending now (status: ${campaign.status}). \`sms-campaigns cancel\` can only stop the remaining sends.
25667
+ `
25668
+ );
25669
+ }
25670
+ });
25671
+ });
25672
+ smsCampaigns.command("cancel").description(
25673
+ "Cancel a scheduled or in-flight SMS campaign \u2014 stops REMAINING sends only; messages already handed to the provider are out"
25674
+ ).argument("<id>", "SMS campaign id").action(async (id, _options, command) => {
25675
+ const flags = getGlobalFlags(command);
25676
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25677
+ const result = await client.cancelSmsCampaign(id);
25678
+ emit(flags, result, () => {
25679
+ const { stats } = result.campaign;
25680
+ process.stdout.write(
25681
+ `Cancelled SMS campaign ${result.campaign.id} \u2014 ${stats.sent} recipient${stats.sent === 1 ? " was" : "s were"} already sent to before the cancel.
25682
+ `
25683
+ );
25684
+ });
25685
+ });
25686
+ smsCampaigns.command("recipients").description("List an SMS campaign's recipients (report drill-down), optionally by status").argument("<id>", "SMS campaign id").option("--status <status>", `filter to one status (${RECIPIENT_STATUSES2.join(" | ")})`).option("--limit <n>", "page size (1-200, default 50)").option("--cursor <cursor>", "cursor from a previous page").action(
25687
+ async (id, options, command) => {
25688
+ const flags = getGlobalFlags(command);
25689
+ let status;
25690
+ if (options.status !== void 0) {
25691
+ const match = RECIPIENT_STATUSES2.find((s) => s === options.status);
25692
+ if (!match) {
25693
+ throw new Error(
25694
+ `Invalid --status: ${options.status} (expected one of ${RECIPIENT_STATUSES2.join(", ")})`
25695
+ );
25696
+ }
25697
+ status = match;
25698
+ }
25699
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25700
+ const page = await client.listSmsCampaignRecipients(id, {
25701
+ status,
25702
+ limit: options.limit !== void 0 ? parsePositiveInt15(options.limit, "--limit") : void 0,
25703
+ cursor: options.cursor
25704
+ });
25705
+ emit(flags, page, () => {
25706
+ printTable(
25707
+ page.recipients.map((r) => ({
25708
+ id: r.id,
25709
+ phone: r.phoneNumber,
25710
+ name: [r.firstName, r.lastName].filter(Boolean).join(" "),
25711
+ status: r.status,
25712
+ parts: r.segments ?? "",
25713
+ processedAt: r.processedAt ?? "",
25714
+ details: r.statusDetails ?? ""
25715
+ })),
25716
+ ["id", "phone", "name", "status", "parts", "processedAt", "details"]
25717
+ );
25718
+ if (page.nextCursor) {
25719
+ process.stdout.write(`
25720
+ Next page: --cursor ${page.nextCursor}
25721
+ `);
25722
+ }
25723
+ });
25724
+ }
25725
+ );
25726
+ }
25727
+
25728
+ // src/commands/social/index.ts
25729
+ var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
25730
+ function composeInput(options) {
25731
+ if (!options.text) {
25732
+ throw new Error("Provide the post text via --text.");
25733
+ }
25734
+ const targets = splitList(options.targets);
25735
+ if (targets.length === 0) {
25736
+ throw new Error("Provide at least one target via --targets (see `social accounts`).");
25737
+ }
25738
+ const mediaIds = splitList(options.media);
25739
+ return {
25740
+ text: options.text,
25741
+ targets,
25742
+ ...mediaIds.length > 0 ? { mediaIds } : {},
25743
+ ...options.firstComment ? { firstComment: options.firstComment } : {},
25744
+ ...options.at ? { scheduledAt: options.at } : {},
25745
+ ...options.publish ? { publish: true } : {}
25746
+ };
25747
+ }
25748
+ function registerSocialCommands(program2) {
24355
25749
  const social = program2.command("social").description("Social media posts \u2014 connected accounts, drafts, scheduling, publishing");
24356
25750
  social.command("accounts").description("List connected social accounts (the values to pass as --targets)").action(async (_, command) => {
24357
25751
  const flags = getGlobalFlags(command);
@@ -24517,7 +25911,7 @@ function assertIsoDate3(value, flag) {
24517
25911
  }
24518
25912
  return value;
24519
25913
  }
24520
- function parsePositiveInt14(value, flag) {
25914
+ function parsePositiveInt16(value, flag) {
24521
25915
  const n = Number.parseInt(value, 10);
24522
25916
  if (!Number.isInteger(n) || n <= 0) {
24523
25917
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
@@ -24706,7 +26100,7 @@ function registerStatsCommands(program2) {
24706
26100
  const flags = getGlobalFlags(command);
24707
26101
  const range = {
24708
26102
  ...resolveRange(options),
24709
- limit: options.limit !== void 0 ? parsePositiveInt14(options.limit, "--limit") : void 0
26103
+ limit: options.limit !== void 0 ? parsePositiveInt16(options.limit, "--limit") : void 0
24710
26104
  };
24711
26105
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24712
26106
  const report = await client.statsSessions(range);
@@ -24767,7 +26161,7 @@ function registerStatsCommands(program2) {
24767
26161
  sessionId: options.session,
24768
26162
  statuses: options.status !== void 0 ? parseConversionStatuses(options.status) : void 0,
24769
26163
  clients: options.client !== void 0 ? parseConversionClients(options.client) : void 0,
24770
- limit: options.limit !== void 0 ? parsePositiveInt14(options.limit, "--limit") : void 0
26164
+ limit: options.limit !== void 0 ? parsePositiveInt16(options.limit, "--limit") : void 0
24771
26165
  });
24772
26166
  emit(flags, result, () => {
24773
26167
  printTable(
@@ -24812,7 +26206,7 @@ function registerStatsCommands(program2) {
24812
26206
  const flags = getGlobalFlags(command);
24813
26207
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24814
26208
  const result = await client.getConversionTrackingHealth(
24815
- options.sinceDays !== void 0 ? parsePositiveInt14(options.sinceDays, "--since-days") : void 0
26209
+ options.sinceDays !== void 0 ? parsePositiveInt16(options.sinceDays, "--since-days") : void 0
24816
26210
  );
24817
26211
  emit(flags, result, () => {
24818
26212
  process.stdout.write(`Conversion tracking health since ${result.since}
@@ -24878,7 +26272,7 @@ function describeAttempts(item) {
24878
26272
  const next = item.nextPaymentAttempt !== null ? formatEpochSeconds(item.nextPaymentAttempt) : "";
24879
26273
  return next ? `${attempts} (next ${next})` : String(attempts);
24880
26274
  }
24881
- function requireConfirm11(confirm, what) {
26275
+ function requireConfirm13(confirm, what) {
24882
26276
  if (!confirm) {
24883
26277
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
24884
26278
  }
@@ -24952,7 +26346,7 @@ function registerSubscriptionsCommands(program2) {
24952
26346
  "Retry a failed subscription charge by its Stripe invoice id (`in_\u2026` from `subscriptions failed`) \u2014 a REAL charge attempt on the customer's saved card. Requires --confirm"
24953
26347
  ).argument("<invoiceId>", "Stripe invoice id of the failed charge (in_\u2026)").option("--confirm", "required \u2014 acknowledge that the customer is charged for real").action(async (invoiceId, options, command) => {
24954
26348
  const flags = getGlobalFlags(command);
24955
- requireConfirm11(options.confirm, `retry the charge for invoice ${invoiceId} (real charge)`);
26349
+ requireConfirm13(options.confirm, `retry the charge for invoice ${invoiceId} (real charge)`);
24956
26350
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24957
26351
  const result = await client.retrySubscriptionPayment(invoiceId);
24958
26352
  emit(flags, result, () => {
@@ -25171,7 +26565,7 @@ function parseChoice8(value, allowed, flag) {
25171
26565
  }
25172
26566
  return match;
25173
26567
  }
25174
- function parsePositiveInt15(value, flag) {
26568
+ function parsePositiveInt17(value, flag) {
25175
26569
  const n = Number.parseInt(value, 10);
25176
26570
  if (!Number.isInteger(n) || n <= 0) {
25177
26571
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
@@ -25303,7 +26697,7 @@ function registerTestimonialsCommands(program2) {
25303
26697
  status: options.status !== void 0 ? parseChoice8(options.status, TESTIMONIAL_STATUSES, "--status") : void 0,
25304
26698
  productId: options.product,
25305
26699
  tag: options.tag,
25306
- limit: options.limit !== void 0 ? parsePositiveInt15(options.limit, "--limit") : void 0,
26700
+ limit: options.limit !== void 0 ? parsePositiveInt17(options.limit, "--limit") : void 0,
25307
26701
  cursor: options.cursor
25308
26702
  });
25309
26703
  emit(flags, page, () => {
@@ -25489,22 +26883,180 @@ Next page: --cursor ${page.nextCursor}
25489
26883
  }
25490
26884
 
25491
26885
  // src/commands/workflows/index.ts
25492
- import { readFileSync as readFileSync22 } from "fs";
25493
- function parsePositiveInt16(value, flag) {
26886
+ import { readFileSync as readFileSync25 } from "fs";
26887
+
26888
+ // src/commands/workflows/authoring.ts
26889
+ import { writeFileSync as writeFileSync6 } from "fs";
26890
+ var GRAPH_INVALID_STATUS = 422;
26891
+ function withGraphHint(error) {
26892
+ if (error instanceof ApiError && error.status === GRAPH_INVALID_STATUS) {
26893
+ throw new ApiError(
26894
+ error.status,
26895
+ error.apiMessage,
26896
+ "The workflow graph did not validate \u2014 re-run with -o json (or --verbose) to see every issue with its node id and reason code, fix the draft, then try again."
26897
+ );
26898
+ }
26899
+ throw error;
26900
+ }
26901
+ function describe(automation) {
26902
+ return `${automation.id} (${automation.name}, ${automation.status})`;
26903
+ }
26904
+ function registerWorkflowAuthoringCommands(workflows) {
26905
+ workflows.command("create").description(
26906
+ "Create a DRAFT workflow \u2014 nothing runs until `workflows publish`. Without --trigger the canvas starts empty; build the graph with `workflows draft set` or in the dashboard"
26907
+ ).requiredOption("--name <name>", "workflow name").option("--description <text>", "what the workflow is for").option(
26908
+ "--product <ids>",
26909
+ "comma-separated product ids the workflow applies to (omit = workspace-wide)",
26910
+ parseCommaList("--product")
26911
+ ).option(
26912
+ "--trigger <eventType>",
26913
+ "business event that starts it, e.g. product.ProductPurchased (see `workflows list` for examples)"
26914
+ ).action(
26915
+ async (options, command) => {
26916
+ const flags = getGlobalFlags(command);
26917
+ const input = { name: options.name };
26918
+ if (options.description !== void 0) input.description = options.description;
26919
+ if (options.product !== void 0) input.productIds = options.product;
26920
+ if (options.trigger !== void 0) input.triggerEventType = options.trigger;
26921
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26922
+ const result = await client.createAutomation(input);
26923
+ emit(flags, result, () => {
26924
+ process.stdout.write(
26925
+ `Created draft workflow ${describe(result.automation)}. Nothing runs until \`workflows publish\`.
26926
+ `
26927
+ );
26928
+ });
26929
+ }
26930
+ );
26931
+ const draft = workflows.command("draft").description("Edit a workflow's DRAFT graph (the published version keeps running untouched)");
26932
+ draft.command("get").description(
26933
+ "Print the draft graph as JSON \u2014 edit it and feed it back to `workflows draft set`. --published prints the live snapshot instead"
26934
+ ).argument("<automationId>", "workflow id").option("--published", "print the PUBLISHED graph (what is running) instead of the draft").option("--out <file>", "write the graph JSON to a file instead of stdout").action(
26935
+ async (automationId, options, command) => {
26936
+ const flags = getGlobalFlags(command);
26937
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26938
+ const workflow = await client.getAutomationGraph(automationId);
26939
+ if (workflow.draftGraph === void 0) {
26940
+ throw new Error(
26941
+ "This API does not return workflow graphs yet (it predates `?include=graph`) \u2014 update the API, or copy the graph from the dashboard builder."
26942
+ );
26943
+ }
26944
+ const graph = options.published ? workflow.publishedGraph : workflow.draftGraph;
26945
+ if (graph === null || graph === void 0) {
26946
+ throw new Error(
26947
+ `Workflow ${automationId} has never been published \u2014 there is no live graph.`
26948
+ );
26949
+ }
26950
+ const json = `${JSON.stringify(graph, null, 2)}
26951
+ `;
26952
+ if (options.out !== void 0) {
26953
+ writeFileSync6(options.out, json);
26954
+ emit(flags, { automationId, out: options.out }, () => {
26955
+ process.stdout.write(
26956
+ `Wrote the ${options.published ? "published" : "draft"} graph of ${automationId} to ${options.out}${workflow.hasUnpublishedChanges ? " (the draft has unpublished changes)" : ""}.
26957
+ `
26958
+ );
26959
+ });
26960
+ return;
26961
+ }
26962
+ process.stdout.write(json);
26963
+ }
26964
+ );
26965
+ draft.command("set").description(
26966
+ "Replace the whole draft graph ({ nodes, edges }) \u2014 the same JSON the dashboard builder saves. The PUBLISHED version is not affected until `workflows publish`"
26967
+ ).argument("<automationId>", "workflow id").requiredOption(
26968
+ "--json <fileOrJson>",
26969
+ "the draft graph as a .json file path or an inline {\u2026} string",
26970
+ parseJsonObjectFileOrInline("--json")
26971
+ ).option("--name <name>", "also rename the workflow").option("--description <text>", "also change the description").option(
26972
+ "--product <ids>",
26973
+ "also replace the product scope (comma-separated ids)",
26974
+ parseCommaList("--product")
26975
+ ).action(
26976
+ async (automationId, options, command) => {
26977
+ const flags = getGlobalFlags(command);
26978
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26979
+ const result = await client.updateAutomationDraft(automationId, {
26980
+ draft: options.json,
26981
+ ...options.name !== void 0 ? { name: options.name } : {},
26982
+ ...options.description !== void 0 ? { description: options.description } : {},
26983
+ ...options.product !== void 0 ? { productIds: options.product } : {}
26984
+ }).catch(withGraphHint);
26985
+ emit(flags, result, () => {
26986
+ if (result.issues.length > 0) {
26987
+ process.stdout.write(
26988
+ `Saved the draft of workflow ${automationId}, but it would NOT publish yet \u2014 ${result.issues.length} issue${result.issues.length === 1 ? "" : "s"}:
26989
+ `
26990
+ );
26991
+ for (const issue of result.issues) {
26992
+ const where = issue.nodeId ?? issue.edgeId;
26993
+ process.stdout.write(
26994
+ ` - [${issue.reason}]${where ? ` ${where}:` : ""} ${issue.message}
26995
+ `
26996
+ );
26997
+ }
26998
+ return;
26999
+ }
27000
+ process.stdout.write(
27001
+ `Saved the draft of workflow ${automationId} \u2014 it validates. Publish it with \`workflows publish ${automationId} --confirm\`.
27002
+ `
27003
+ );
27004
+ });
27005
+ }
27006
+ );
27007
+ workflows.command("publish").description(
27008
+ "\u26A0\uFE0F Publishes the draft \u2014 the workflow goes LIVE: from now on REAL e-mails, SMS, webhooks and access changes fire on real customer events. The graph must validate. Requires --confirm"
27009
+ ).argument("<automationId>", "workflow id").option("--confirm", "required \u2014 acknowledge the workflow goes live").action(async (automationId, options, command) => {
27010
+ const flags = getGlobalFlags(command);
27011
+ requireConfirm(options.confirm, `publish workflow ${automationId} (it goes live)`);
27012
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27013
+ const result = await client.publishAutomation(automationId).catch(withGraphHint);
27014
+ emit(flags, result, () => {
27015
+ process.stdout.write(
27016
+ `Published workflow ${describe(result.automation)}. Pause it any time with \`workflows pause\`.
27017
+ `
27018
+ );
27019
+ });
27020
+ });
27021
+ workflows.command("duplicate").description("Duplicate a workflow \u2014 the copy is an unpublished draft").argument("<automationId>", "workflow id").option("--name <name>", "name of the copy (default: the original name with a copy suffix)").action(async (automationId, options, command) => {
27022
+ const flags = getGlobalFlags(command);
27023
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27024
+ const result = await client.duplicateAutomation(automationId, options.name);
27025
+ emit(flags, result, () => {
27026
+ process.stdout.write(`Duplicated ${automationId} \u2192 ${describe(result.automation)}.
27027
+ `);
27028
+ });
27029
+ });
27030
+ workflows.command("delete").description(
27031
+ "PERMANENTLY delete a workflow \u2014 cannot be undone. `workflows pause` is the reversible alternative. Requires --confirm"
27032
+ ).argument("<automationId>", "workflow id").option("--confirm", "required \u2014 acknowledge the permanent deletion").action(async (automationId, options, command) => {
27033
+ const flags = getGlobalFlags(command);
27034
+ requireConfirm(options.confirm, `delete workflow ${automationId}`);
27035
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27036
+ const result = await client.deleteAutomation(automationId);
27037
+ emit(flags, result, () => {
27038
+ process.stdout.write(`Deleted workflow ${automationId}.
27039
+ `);
27040
+ });
27041
+ });
27042
+ }
27043
+
27044
+ // src/commands/workflows/index.ts
27045
+ function parsePositiveInt18(value, flag) {
25494
27046
  const n = Number.parseInt(value, 10);
25495
27047
  if (!Number.isInteger(n) || n <= 0) {
25496
27048
  throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
25497
27049
  }
25498
27050
  return n;
25499
27051
  }
25500
- function requireConfirm12(confirm, what) {
27052
+ function requireConfirm14(confirm, what) {
25501
27053
  if (!confirm) {
25502
27054
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
25503
27055
  }
25504
27056
  }
25505
27057
  function parseTriggerContext(value) {
25506
27058
  const trimmed = value.trim();
25507
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync22(value, "utf8");
27059
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync25(value, "utf8");
25508
27060
  let parsed;
25509
27061
  try {
25510
27062
  parsed = JSON.parse(raw);
@@ -25518,9 +27070,9 @@ function parseTriggerContext(value) {
25518
27070
  }
25519
27071
  return parsed;
25520
27072
  }
25521
- var SCHEDULER_UNAVAILABLE_STATUS4 = 503;
25522
- function withSchedulerHint4(error) {
25523
- if (error instanceof ApiError && error.status === SCHEDULER_UNAVAILABLE_STATUS4) {
27073
+ var SCHEDULER_UNAVAILABLE_STATUS5 = 503;
27074
+ function withSchedulerHint5(error) {
27075
+ if (error instanceof ApiError && error.status === SCHEDULER_UNAVAILABLE_STATUS5) {
25524
27076
  throw new ApiError(
25525
27077
  error.status,
25526
27078
  "Trigger.dev scheduler not configured \u2014 workflow runs cannot be started on this environment.",
@@ -25593,7 +27145,7 @@ function registerWorkflowsCommands(program2) {
25593
27145
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25594
27146
  const page = await client.listAutomationRuns(
25595
27147
  automationId,
25596
- options.limit !== void 0 ? parsePositiveInt16(options.limit, "--limit") : void 0
27148
+ options.limit !== void 0 ? parsePositiveInt18(options.limit, "--limit") : void 0
25597
27149
  );
25598
27150
  emit(flags, page, () => {
25599
27151
  printTable(
@@ -25635,9 +27187,9 @@ function registerWorkflowsCommands(program2) {
25635
27187
  ).argument("<automationId>", "workflow automation id").argument("<runId>", "run id (from `workflows runs`)").option("--confirm", "required \u2014 acknowledge that the workflow actions execute again").action(
25636
27188
  async (automationId, runId, options, command) => {
25637
27189
  const flags = getGlobalFlags(command);
25638
- requireConfirm12(options.confirm, `retry run ${runId} (its actions execute again)`);
27190
+ requireConfirm14(options.confirm, `retry run ${runId} (its actions execute again)`);
25639
27191
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25640
- const result = await client.retryAutomationRun(automationId, runId).catch(withSchedulerHint4);
27192
+ const result = await client.retryAutomationRun(automationId, runId).catch(withSchedulerHint5);
25641
27193
  emit(flags, result, () => {
25642
27194
  process.stdout.write(
25643
27195
  `Accepted \u2014 retrying run ${runId} of workflow ${automationId} as new run ${result.runId}.
@@ -25659,7 +27211,7 @@ Follow it with: zanfia workflows runs ${automationId}
25659
27211
  async (automationId, options, command) => {
25660
27212
  const flags = getGlobalFlags(command);
25661
27213
  const context = parseTriggerContext(options.context);
25662
- requireConfirm12(options.confirm, `test-run workflow ${automationId} (real actions fire)`);
27214
+ requireConfirm14(options.confirm, `test-run workflow ${automationId} (real actions fire)`);
25663
27215
  process.stderr.write(
25664
27216
  "Warning: a test run executes the published graph for real \u2014 emails are sent, tags applied, webhooks called. Point the context at a test client.\n"
25665
27217
  );
@@ -25667,7 +27219,7 @@ Follow it with: zanfia workflows runs ${automationId}
25667
27219
  const result = await client.testRunAutomation(automationId, {
25668
27220
  context,
25669
27221
  ...options.triggerNode !== void 0 ? { triggerNodeId: options.triggerNode } : {}
25670
- }).catch(withSchedulerHint4);
27222
+ }).catch(withSchedulerHint5);
25671
27223
  emit(flags, result, () => {
25672
27224
  process.stdout.write(
25673
27225
  `Accepted \u2014 test run ${result.runId} started for workflow ${automationId}.
@@ -25677,6 +27229,7 @@ Follow it with: zanfia workflows runs ${automationId}
25677
27229
  });
25678
27230
  }
25679
27231
  );
27232
+ registerWorkflowAuthoringCommands(workflows);
25680
27233
  }
25681
27234
  function formatProductScope(workflow) {
25682
27235
  return workflow.productIds.length > 0 ? String(workflow.productIds.length) : "all";
@@ -25688,10 +27241,111 @@ function formatSteps(steps) {
25688
27241
  return parts.join(", ");
25689
27242
  }
25690
27243
 
27244
+ // src/commands/workspace/index.ts
27245
+ function parseOnOff3(value, flag) {
27246
+ const match = ["on", "off"].find((v) => v === value);
27247
+ if (!match) throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
27248
+ return match === "on";
27249
+ }
27250
+ function parseTranscriptionLanguage(value) {
27251
+ const match = TRANSCRIPTION_LANGUAGES.find((language) => language === value);
27252
+ if (!match) {
27253
+ throw new Error(
27254
+ `Invalid --transcription-language: ${value} (expected ${TRANSCRIPTION_LANGUAGES.join(" | ")})`
27255
+ );
27256
+ }
27257
+ return match;
27258
+ }
27259
+ function assertTimeZone(value) {
27260
+ try {
27261
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
27262
+ } catch {
27263
+ throw new Error(
27264
+ `Invalid --timezone: ${value} (expected an IANA name such as Europe/Warsaw or America/New_York)`
27265
+ );
27266
+ }
27267
+ }
27268
+ function printSettings(settings) {
27269
+ process.stdout.write(`Workspace ${settings.workspaceId}
27270
+ `);
27271
+ process.stdout.write(` Name: ${settings.workspaceName}
27272
+ `);
27273
+ process.stdout.write(
27274
+ ` Timezone: ${settings.timezone ?? "(inherits the owner's account timezone)"}
27275
+ `
27276
+ );
27277
+ process.stdout.write(
27278
+ ` Auto-transcribe uploads: ${settings.autoTranscribeUploads ? "on" : "off"}
27279
+ `
27280
+ );
27281
+ process.stdout.write(` Transcription language: ${settings.transcriptionLanguage}
27282
+ `);
27283
+ }
27284
+ function registerWorkspaceCommands(program2) {
27285
+ const workspace2 = program2.command("workspace").description(
27286
+ 'Workspace-level settings: the name, the scheduling timezone (workflow "wait until" steps and other workspace-scoped scheduling) and the video transcription defaults'
27287
+ );
27288
+ workspace2.command("get", { isDefault: true }).description("Show the workspace settings").action(async (_options, command) => {
27289
+ const flags = getGlobalFlags(command);
27290
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27291
+ const result = await client.getWorkspaceSettings();
27292
+ emit(flags, result, () => printSettings(result));
27293
+ });
27294
+ workspace2.command("update").description(
27295
+ "Change workspace settings. The video quality profile is plan-dependent and stays in the dashboard"
27296
+ ).option("--name <name>", "workspace name (1-100 characters)").option("--timezone <iana>", "scheduling timezone, e.g. Europe/Warsaw").option("--inherit-timezone", "clear the override and follow the owner's account timezone").option("--auto-transcribe <on|off>", "transcribe newly uploaded videos automatically").option(
27297
+ "--transcription-language <language>",
27298
+ `default transcription language (${TRANSCRIPTION_LANGUAGES.join(" | ")})`
27299
+ ).action(
27300
+ async (options, command) => {
27301
+ const flags = getGlobalFlags(command);
27302
+ if (options.timezone !== void 0 && options.inheritTimezone) {
27303
+ throw new Error("Use either --timezone or --inherit-timezone, not both.");
27304
+ }
27305
+ const input = {};
27306
+ if (options.name !== void 0) input.name = options.name;
27307
+ if (options.timezone !== void 0) {
27308
+ assertTimeZone(options.timezone);
27309
+ input.timezone = options.timezone;
27310
+ }
27311
+ if (options.inheritTimezone) input.timezone = null;
27312
+ if (options.autoTranscribe !== void 0) {
27313
+ input.autoTranscribeUploads = parseOnOff3(options.autoTranscribe, "--auto-transcribe");
27314
+ }
27315
+ if (options.transcriptionLanguage !== void 0) {
27316
+ input.transcriptionLanguage = parseTranscriptionLanguage(options.transcriptionLanguage);
27317
+ }
27318
+ if (Object.keys(input).length === 0) {
27319
+ throw new Error(
27320
+ "Nothing to update \u2014 pass at least one change flag (see `workspace update --help`)."
27321
+ );
27322
+ }
27323
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27324
+ const result = await client.updateWorkspaceSettings(input);
27325
+ emit(flags, result, () => printSettings(result));
27326
+ }
27327
+ );
27328
+ }
27329
+
25691
27330
  // src/program.ts
27331
+ function resolveCliVersion() {
27332
+ if ("0.3.47".length > 0) {
27333
+ return "0.3.47";
27334
+ }
27335
+ try {
27336
+ const raw = readFileSync26(new URL("../package.json", import.meta.url), "utf8");
27337
+ const parsed = JSON.parse(raw);
27338
+ if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
27339
+ const { version: version2 } = parsed;
27340
+ if (typeof version2 === "string") return version2;
27341
+ }
27342
+ } catch {
27343
+ }
27344
+ return "0.0.0";
27345
+ }
25692
27346
  function createProgram(programName) {
25693
27347
  const program2 = new Command();
25694
- program2.name(programName).description("Zanfia CLI \u2014 manage products, clients, orders and more from your terminal").version("0.3.26").showHelpAfterError().addHelpText(
27348
+ program2.name(programName).description("Zanfia CLI \u2014 manage products, clients, orders and more from your terminal").version(resolveCliVersion()).showHelpAfterError().addHelpText(
25695
27349
  "after",
25696
27350
  `
25697
27351
  New to Zanfia?
@@ -25733,6 +27387,7 @@ Signing in \u2014 two roles, two consent pages:
25733
27387
  registerConversationsCommands(program2);
25734
27388
  registerClientsCommands(program2);
25735
27389
  registerCampaignsCommands(program2);
27390
+ registerSmsCampaignsCommands(program2);
25736
27391
  registerSegmentsCommands(program2);
25737
27392
  registerFormsCommands(program2);
25738
27393
  registerTestimonialsCommands(program2);
@@ -25745,6 +27400,7 @@ Signing in \u2014 two roles, two consent pages:
25745
27400
  registerReferralsCommands(program2);
25746
27401
  registerAdsCommands(program2);
25747
27402
  registerWorkflowsCommands(program2);
27403
+ registerWorkspaceCommands(program2);
25748
27404
  registerFunnelsCommands(program2);
25749
27405
  registerPagesCommands(program2);
25750
27406
  registerSocialCommands(program2);