@zanfia/cli 0.3.47 → 0.3.63

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 +2786 -283
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -980,7 +980,7 @@ var require_command = __commonJS({
980
980
  var { Help: Help2 } = require_help();
981
981
  var { Option: Option2, DualOptions } = require_option();
982
982
  var { suggestSimilar } = require_suggestSimilar();
983
- var Command3 = class _Command extends EventEmitter {
983
+ var Command4 = class _Command extends EventEmitter {
984
984
  /**
985
985
  * Initialize a new `Command`.
986
986
  *
@@ -3006,7 +3006,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3006
3006
  return arg;
3007
3007
  });
3008
3008
  }
3009
- exports.Command = Command3;
3009
+ exports.Command = Command4;
3010
3010
  }
3011
3011
  });
3012
3012
 
@@ -3015,15 +3015,15 @@ var require_commander = __commonJS({
3015
3015
  "node_modules/commander/index.js"(exports) {
3016
3016
  "use strict";
3017
3017
  var { Argument: Argument2 } = require_argument();
3018
- var { Command: Command3 } = require_command();
3018
+ var { Command: Command4 } = require_command();
3019
3019
  var { CommanderError: CommanderError2, InvalidArgumentError: InvalidArgumentError2 } = require_error();
3020
3020
  var { Help: Help2 } = require_help();
3021
3021
  var { Option: Option2 } = require_option();
3022
- exports.program = new Command3();
3023
- exports.createCommand = (name) => new Command3(name);
3022
+ exports.program = new Command4();
3023
+ exports.createCommand = (name) => new Command4(name);
3024
3024
  exports.createOption = (flags, description) => new Option2(flags, description);
3025
3025
  exports.createArgument = (name, description) => new Argument2(name, description);
3026
- exports.Command = Command3;
3026
+ exports.Command = Command4;
3027
3027
  exports.Option = Option2;
3028
3028
  exports.Argument = Argument2;
3029
3029
  exports.Help = Help2;
@@ -4004,8 +4004,8 @@ var require_lodash3 = __commonJS({
4004
4004
  }
4005
4005
  var stringToPath = memoize(function(string) {
4006
4006
  var result = [];
4007
- toString(string).replace(rePropName, function(match, number, quote, string2) {
4008
- result.push(quote ? string2.replace(reEscapeChar, "$1") : number || match);
4007
+ toString(string).replace(rePropName, function(match, number, quote2, string2) {
4008
+ result.push(quote2 ? string2.replace(reEscapeChar, "$1") : number || match);
4009
4009
  });
4010
4010
  return result;
4011
4011
  });
@@ -5793,8 +5793,8 @@ var require_graceful_fs = __commonJS({
5793
5793
  fs3.createReadStream = createReadStream3;
5794
5794
  fs3.createWriteStream = createWriteStream;
5795
5795
  var fs$readFile = fs3.readFile;
5796
- fs3.readFile = readFile3;
5797
- function readFile3(path2, options, cb) {
5796
+ fs3.readFile = readFile4;
5797
+ function readFile4(path2, options, cb) {
5798
5798
  if (typeof options === "function")
5799
5799
  cb = options, options = null;
5800
5800
  return go$readFile(path2, options, cb);
@@ -6901,7 +6901,7 @@ var {
6901
6901
  } = import_index.default;
6902
6902
 
6903
6903
  // src/program.ts
6904
- import { readFileSync as readFileSync26 } from "fs";
6904
+ import { readFileSync as readFileSync28 } from "fs";
6905
6905
 
6906
6906
  // src/utils/config.ts
6907
6907
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -6918,8 +6918,17 @@ var backendDefaults = {
6918
6918
  memberExchangeUrl: true ? orUndefined("https://europe-central2-mailingr-54736.cloudfunctions.net/users-v1/member/cli/auth/exchange") : void 0,
6919
6919
  usersUrl: true ? orUndefined("https://europe-central2-mailingr-54736.cloudfunctions.net/users-v1") : void 0,
6920
6920
  /** Firebase web API key — a public client identifier, not a secret. */
6921
- firebaseApiKey: true ? orUndefined("AIzaSyAcbScoToLCbdhVYMCilqvLn9f7RYsG2XU") : void 0
6921
+ firebaseApiKey: true ? orUndefined("AIzaSyAcbScoToLCbdhVYMCilqvLn9f7RYsG2XU") : void 0,
6922
+ /**
6923
+ * Public platform origin that serves the page widgets (`zanfia-page-forms.js`)
6924
+ * and their `/api/*` endpoints — what an embed snippet points at.
6925
+ */
6926
+ platformUrl: true ? orUndefined("https://zanfia.co") : void 0
6922
6927
  };
6928
+ function resolvePlatformUrl() {
6929
+ const raw = process.env.ZANFIA_PLATFORM_URL || backendDefaults.platformUrl || "https://zanfia.co";
6930
+ return raw.replace(/\/+$/, "");
6931
+ }
6923
6932
  function readCredentials() {
6924
6933
  try {
6925
6934
  const raw = readFileSync(CREDENTIALS_FILE, "utf8");
@@ -7996,6 +8005,94 @@ function createMockClient() {
7996
8005
  async getConversionTrackingHealth() {
7997
8006
  return { since: (/* @__PURE__ */ new Date()).toISOString(), integrations: [] };
7998
8007
  },
8008
+ async getIntegrationsOverview() {
8009
+ return {
8010
+ emailClients: [],
8011
+ metaConversionApi: [],
8012
+ conversionTrackingIntegrationsCount: 0,
8013
+ invoicesClients: [],
8014
+ payments: {
8015
+ stripe: false,
8016
+ payU: false,
8017
+ tpay: false,
8018
+ payNow: false,
8019
+ przelewy24: false,
8020
+ paypal: false,
8021
+ dodo: false,
8022
+ manual: null
8023
+ },
8024
+ sms: { defaultProvider: null, twilio: null, smsapi: null }
8025
+ };
8026
+ },
8027
+ async connectEmailClient() {
8028
+ throw new ApiError(501, "integrations are not available in mock mode");
8029
+ },
8030
+ async disconnectEmailClient() {
8031
+ throw new ApiError(501, "integrations are not available in mock mode");
8032
+ },
8033
+ async refreshEmailClientLists() {
8034
+ throw new ApiError(501, "integrations are not available in mock mode");
8035
+ },
8036
+ async connectMetaConversionApi() {
8037
+ throw new ApiError(501, "integrations are not available in mock mode");
8038
+ },
8039
+ async disconnectMetaConversionApi() {
8040
+ throw new ApiError(501, "integrations are not available in mock mode");
8041
+ },
8042
+ async configureManualPayment() {
8043
+ throw new ApiError(501, "integrations are not available in mock mode");
8044
+ },
8045
+ async disconnectManualPayment() {
8046
+ throw new ApiError(501, "integrations are not available in mock mode");
8047
+ },
8048
+ async connectInvoicesClient() {
8049
+ throw new ApiError(501, "integrations are not available in mock mode");
8050
+ },
8051
+ async disconnectInvoicesClient() {
8052
+ throw new ApiError(501, "integrations are not available in mock mode");
8053
+ },
8054
+ async connectPaymentGateway() {
8055
+ throw new ApiError(501, "integrations are not available in mock mode");
8056
+ },
8057
+ async disconnectPaymentGateway() {
8058
+ throw new ApiError(501, "integrations are not available in mock mode");
8059
+ },
8060
+ async setTpayRecurringBlik() {
8061
+ throw new ApiError(501, "integrations are not available in mock mode");
8062
+ },
8063
+ async connectTwilio() {
8064
+ throw new ApiError(501, "integrations are not available in mock mode");
8065
+ },
8066
+ async disconnectTwilio() {
8067
+ throw new ApiError(501, "integrations are not available in mock mode");
8068
+ },
8069
+ async connectSmsApi() {
8070
+ throw new ApiError(501, "integrations are not available in mock mode");
8071
+ },
8072
+ async disconnectSmsApi() {
8073
+ throw new ApiError(501, "integrations are not available in mock mode");
8074
+ },
8075
+ async refreshSmsApiSenders() {
8076
+ throw new ApiError(501, "integrations are not available in mock mode");
8077
+ },
8078
+ async setDefaultSmsApiSender() {
8079
+ throw new ApiError(501, "integrations are not available in mock mode");
8080
+ },
8081
+ async setDefaultSmsProvider() {
8082
+ throw new ApiError(501, "integrations are not available in mock mode");
8083
+ },
8084
+ async listConversionTrackingIntegrations() {
8085
+ return { defaultIntegrationIds: [], integrations: [] };
8086
+ },
8087
+ async upsertConversionTrackingIntegration() {
8088
+ throw new ApiError(501, "conversion-tracking integrations are not available in mock mode");
8089
+ },
8090
+ async removeConversionTrackingIntegration() {
8091
+ throw new ApiError(501, "conversion-tracking integrations are not available in mock mode");
8092
+ },
8093
+ async setDefaultConversionTracking() {
8094
+ throw new ApiError(501, "conversion-tracking integrations are not available in mock mode");
8095
+ },
7999
8096
  async listCommunities() {
8000
8097
  return mockBackend.listCommunities();
8001
8098
  },
@@ -8246,6 +8343,9 @@ function createMockClient() {
8246
8343
  async cancelEmailCampaign() {
8247
8344
  throw new ApiError(501, "email campaigns are not available in mock mode");
8248
8345
  },
8346
+ async resendEmailCampaign() {
8347
+ throw new ApiError(501, "email campaigns are not available in mock mode");
8348
+ },
8249
8349
  async listCampaignRecipients() {
8250
8350
  throw new ApiError(501, "email campaigns are not available in mock mode");
8251
8351
  },
@@ -8441,6 +8541,15 @@ function createMockClient() {
8441
8541
  async copyNewsletterIssues() {
8442
8542
  throw new ApiError(501, "newsletter content is not available in mock mode");
8443
8543
  },
8544
+ async sendNewsletterIssue() {
8545
+ throw new ApiError(501, "newsletter content is not available in mock mode");
8546
+ },
8547
+ async cancelNewsletterIssueSend() {
8548
+ throw new ApiError(501, "newsletter content is not available in mock mode");
8549
+ },
8550
+ async getNewsletterIssueSendReport() {
8551
+ throw new ApiError(501, "newsletter content is not available in mock mode");
8552
+ },
8444
8553
  async getKbContent() {
8445
8554
  throw new ApiError(501, "knowledge-base content is not available in mock mode");
8446
8555
  },
@@ -8480,6 +8589,21 @@ function createMockClient() {
8480
8589
  async getMediaTranscript() {
8481
8590
  throw new ApiError(501, "media transcripts are not available in mock mode");
8482
8591
  },
8592
+ async listMediaCaptions() {
8593
+ throw new ApiError(501, "video captions are not available in mock mode");
8594
+ },
8595
+ async uploadMediaCaption() {
8596
+ throw new ApiError(501, "video captions are not available in mock mode");
8597
+ },
8598
+ async getMediaCaption() {
8599
+ throw new ApiError(501, "video captions are not available in mock mode");
8600
+ },
8601
+ async deleteMediaCaption() {
8602
+ throw new ApiError(501, "video captions are not available in mock mode");
8603
+ },
8604
+ async regenerateMediaCaption() {
8605
+ throw new ApiError(501, "video captions are not available in mock mode");
8606
+ },
8483
8607
  async generateAIImage() {
8484
8608
  throw new ApiError(501, "AI image generation is not available in mock mode");
8485
8609
  },
@@ -8609,6 +8733,12 @@ function createMockClient() {
8609
8733
  async listSignupFormSubmissions() {
8610
8734
  return { submissions: [] };
8611
8735
  },
8736
+ async listLiveRooms() {
8737
+ return { rooms: [] };
8738
+ },
8739
+ async getLiveRoomAttendance() {
8740
+ throw new ApiError(501, "live rooms are not available in mock mode");
8741
+ },
8612
8742
  async call(method, pathTemplate) {
8613
8743
  throw new ApiError(501, `${method} ${pathTemplate} is not available in mock mode`);
8614
8744
  },
@@ -8933,7 +9063,10 @@ function createHttpClient(config) {
8933
9063
  "GET",
8934
9064
  productId ? `/checkouts?productId=${encodeURIComponent(productId)}` : "/checkouts"
8935
9065
  ),
8936
- getCheckout: (id) => request("GET", `/checkouts/${encodeURIComponent(id)}`),
9066
+ getCheckout: (id, options) => request(
9067
+ "GET",
9068
+ options?.full ? `/checkouts/${encodeURIComponent(id)}?include=full` : `/checkouts/${encodeURIComponent(id)}`
9069
+ ),
8937
9070
  createCheckout: (input) => request("POST", "/checkouts", input),
8938
9071
  updateCheckout: (id, input) => request("PATCH", `/checkouts/${encodeURIComponent(id)}`, input),
8939
9072
  publishCheckout: (id, active) => request("POST", `/checkouts/${encodeURIComponent(id)}/publish`, { active }),
@@ -9195,6 +9328,33 @@ function createHttpClient(config) {
9195
9328
  "GET",
9196
9329
  `/conversion-tracking/health${sinceDays !== void 0 ? `?sinceDays=${sinceDays}` : ""}`
9197
9330
  ),
9331
+ getIntegrationsOverview: () => request("GET", "/integrations"),
9332
+ connectEmailClient: (input) => request("POST", "/integrations/email-clients", input),
9333
+ disconnectEmailClient: (client, url) => request("POST", "/integrations/email-clients/disconnect", { client, url }),
9334
+ refreshEmailClientLists: (input) => request("POST", "/integrations/email-clients/refresh-lists", input),
9335
+ connectMetaConversionApi: (input) => request("PUT", "/integrations/meta-conversion-api", input),
9336
+ disconnectMetaConversionApi: (integrationId) => request("DELETE", `/integrations/meta-conversion-api/${encodeURIComponent(integrationId)}`),
9337
+ configureManualPayment: (input) => request("PUT", "/integrations/payments/manual", input),
9338
+ disconnectManualPayment: () => request("DELETE", "/integrations/payments/manual"),
9339
+ connectInvoicesClient: (input) => request("POST", "/integrations/invoices-clients", input),
9340
+ disconnectInvoicesClient: (client, id) => request("POST", "/integrations/invoices-clients/disconnect", { client, id }),
9341
+ connectPaymentGateway: (input) => request("POST", "/integrations/payments/gateways", input),
9342
+ disconnectPaymentGateway: (gateway) => request("POST", "/integrations/payments/gateways/disconnect", { gateway }),
9343
+ setTpayRecurringBlik: (enabled) => request("PUT", "/integrations/payments/tpay/recurring-blik", { enabled }),
9344
+ connectTwilio: (input) => request("PUT", "/integrations/sms/twilio", input),
9345
+ disconnectTwilio: () => request("DELETE", "/integrations/sms/twilio"),
9346
+ connectSmsApi: (input) => request("PUT", "/integrations/sms/smsapi", input),
9347
+ disconnectSmsApi: () => request("DELETE", "/integrations/sms/smsapi"),
9348
+ refreshSmsApiSenders: () => request("POST", "/integrations/sms/smsapi/refresh-senders"),
9349
+ setDefaultSmsApiSender: (sender) => request("PUT", "/integrations/sms/smsapi/default-sender", { sender }),
9350
+ setDefaultSmsProvider: (provider) => request("PUT", "/integrations/sms/default-provider", { provider }),
9351
+ listConversionTrackingIntegrations: () => request("GET", "/conversion-tracking/integrations"),
9352
+ upsertConversionTrackingIntegration: (input) => request("PUT", "/conversion-tracking/integrations", input),
9353
+ removeConversionTrackingIntegration: (client, integrationId) => request(
9354
+ "DELETE",
9355
+ `/conversion-tracking/integrations/${encodeURIComponent(client)}/${encodeURIComponent(integrationId)}`
9356
+ ),
9357
+ setDefaultConversionTracking: (integrationIds) => request("PUT", "/conversion-tracking/defaults", { integrationIds: [...integrationIds] }),
9198
9358
  statsProduct: (productId, currency) => request("GET", `/products/${encodeURIComponent(productId)}/stats${statsQuery({ currency })}`),
9199
9359
  listCommunities: () => communityRequest("GET", "/v1/communities"),
9200
9360
  listCommunityChannels: (communityId) => communityRequest("GET", `/v1/communities/${encodeURIComponent(communityId)}/channels`),
@@ -9375,6 +9535,7 @@ function createHttpClient(config) {
9375
9535
  scheduledAt !== void 0 ? { scheduledAt } : {}
9376
9536
  ),
9377
9537
  cancelEmailCampaign: (id) => request("POST", `/campaigns/${encodeURIComponent(id)}/cancel`),
9538
+ resendEmailCampaign: (id, target) => request("POST", `/campaigns/${encodeURIComponent(id)}/resend`, target ? { target } : {}),
9378
9539
  listCampaignRecipients: (id, options) => {
9379
9540
  const params = new URLSearchParams();
9380
9541
  if (options?.status) params.set("status", options.status);
@@ -9534,6 +9695,19 @@ function createHttpClient(config) {
9534
9695
  `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/draft`
9535
9696
  ),
9536
9697
  copyNewsletterIssues: (productId, input) => request("POST", `/products/${encodeURIComponent(productId)}/newsletter/issues/copy`, input),
9698
+ sendNewsletterIssue: (productId, issueId, input) => request(
9699
+ "POST",
9700
+ `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/send`,
9701
+ input
9702
+ ),
9703
+ cancelNewsletterIssueSend: (productId, issueId) => request(
9704
+ "POST",
9705
+ `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/send/cancel`
9706
+ ),
9707
+ getNewsletterIssueSendReport: (productId, issueId) => request(
9708
+ "GET",
9709
+ `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/send/report`
9710
+ ),
9537
9711
  // Knowledge-base product content (addressed by product id).
9538
9712
  getKbContent: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/kb`),
9539
9713
  getKbArticle: (productId, articleId) => request(
@@ -9574,6 +9748,17 @@ function createHttpClient(config) {
9574
9748
  "GET",
9575
9749
  `/media/${encodeURIComponent(mediaId)}/transcript${includeWords ? "?includeWords=true" : ""}`
9576
9750
  ),
9751
+ listMediaCaptions: (mediaId) => request("GET", `/media/${encodeURIComponent(mediaId)}/captions`),
9752
+ uploadMediaCaption: (mediaId, input) => request("POST", `/media/${encodeURIComponent(mediaId)}/captions`, input),
9753
+ getMediaCaption: (mediaId, srclang) => request(
9754
+ "GET",
9755
+ `/media/${encodeURIComponent(mediaId)}/captions/${encodeURIComponent(srclang)}`
9756
+ ),
9757
+ deleteMediaCaption: (mediaId, srclang) => request(
9758
+ "DELETE",
9759
+ `/media/${encodeURIComponent(mediaId)}/captions/${encodeURIComponent(srclang)}`
9760
+ ),
9761
+ regenerateMediaCaption: (mediaId) => request("POST", `/media/${encodeURIComponent(mediaId)}/captions/regenerate`, {}),
9577
9762
  // AI image generation + on-demand transcription (both spend credits / minutes).
9578
9763
  generateAIImage: (input) => request("POST", "/media/ai-images", input),
9579
9764
  transcribeMedia: (mediaId, input) => request("POST", `/media/${encodeURIComponent(mediaId)}/transcribe`, input),
@@ -9713,6 +9898,12 @@ function createHttpClient(config) {
9713
9898
  "GET",
9714
9899
  `/forms/${encodeURIComponent(id)}/submissions${limit !== void 0 ? `?limit=${limit}` : ""}`
9715
9900
  ),
9901
+ // Live rooms live on the Workspace API (api-v1), read only.
9902
+ listLiveRooms: () => request("GET", "/live-rooms"),
9903
+ getLiveRoomAttendance: (roomId, session) => request(
9904
+ "GET",
9905
+ `/live-rooms/${encodeURIComponent(roomId)}/attendance${session !== void 0 ? `?session=${encodeURIComponent(session)}` : ""}`
9906
+ ),
9716
9907
  // Descriptor-generated commands (certificates, …) — Workspace API (api-v1).
9717
9908
  call: (method, pathTemplate, params, query, body) => request(method, `${fillPathTemplate(pathTemplate, params)}${toQuery(query)}`, body),
9718
9909
  // Referral / affiliate programs live on the Workspace API (api-v1).
@@ -9819,6 +10010,14 @@ function parseNumberOption(flag, format) {
9819
10010
  return n;
9820
10011
  };
9821
10012
  }
10013
+ var WORKFLOW_KINDS = ["transactional", "marketing"];
10014
+ function parseWorkflowKind(value) {
10015
+ const kind = value.trim().toLowerCase();
10016
+ if (!WORKFLOW_KINDS.includes(kind)) {
10017
+ throw new Error(`Invalid --kind: ${value} (expected ${WORKFLOW_KINDS.join(" or ")})`);
10018
+ }
10019
+ return kind;
10020
+ }
9822
10021
  function parseCommaList(flag) {
9823
10022
  return (value) => {
9824
10023
  const items = [
@@ -10222,9 +10421,133 @@ function registerCertificatesCommands(program2) {
10222
10421
  });
10223
10422
  }
10224
10423
 
10424
+ // src/commands/_generated/blog.ts
10425
+ var listBlogPostsColumns = ["id", "slug", "title", "status", "publishedAt", "updatedAt"];
10426
+ function registerBlogCommands(program2) {
10427
+ const blog = program2.command("blog");
10428
+ blog.description(
10429
+ "The workspace blog (Your site \u2192 Blog): Markdown posts served at /blog on the store host; drafts are visible to the team only"
10430
+ );
10431
+ blog.command("list").summary("List blog posts").description(
10432
+ "Every post of the workspace blog (drafts and published), newest change first: id, slug (`/blog/{slug}` on the store host), title, excerpt, cover, tags, status and dates. No body - use get-blog-post for the Markdown. The id source for every other blog tool."
10433
+ ).action(async (_options, command) => {
10434
+ const flags = getGlobalFlags(command);
10435
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10436
+ const params = {};
10437
+ const result = await client.call("GET", "/blog-posts", params, void 0, void 0);
10438
+ const rows = tableRowsOf(result, "posts");
10439
+ emit(flags, result ?? {}, () => printTable(rows, listBlogPostsColumns));
10440
+ });
10441
+ blog.command("create").summary("Create a blog post").description(
10442
+ "Creates a blog post as a DRAFT (or published with `publish: true` when it has a body). The body is Markdown. A slug the caller sets must be free (409) and not `feed`; a slug derived from the title takes the next free numeric suffix. Returns the full post. Requires the blog early access on the workspace (403 otherwise)."
10443
+ ).requiredOption("--title <value>", "Post title (max 200 characters).").option(
10444
+ "--slug <value>",
10445
+ "Derived from the title when omitted; a taken derived slug gets a numeric suffix."
10446
+ ).option("--content <value>", "The body as Markdown (headings, lists, links, images by URL).").option(
10447
+ "--excerpt <value>",
10448
+ "Short plain-text summary (list, RSS, meta description); derived from the body on publish when empty."
10449
+ ).option("--cover <value>", "Cover image URL (Media Library).").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option("--seo <json|file>", "seo", parseJsonObjectFileOrInline("--seo")).option("--publish", "Publish right away (needs a non-empty body); default false = draft.").option("--no-publish", "Send `publish: false` explicitly.").action(async (options, command) => {
10450
+ const flags = getGlobalFlags(command);
10451
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10452
+ const params = {};
10453
+ const payload = {
10454
+ title: options.title,
10455
+ slug: options.slug,
10456
+ content: options.content,
10457
+ excerpt: options.excerpt,
10458
+ coverImageUrl: options.cover,
10459
+ tags: options.tags,
10460
+ seo: options.seo,
10461
+ publish: options.publish
10462
+ };
10463
+ const result = await client.call("POST", "/blog-posts", params, void 0, payload);
10464
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10465
+ });
10466
+ blog.command("get").summary("Blog post detail").description(
10467
+ "One blog post with its Markdown body and SEO overrides (title, description, noindex). 404 when unknown."
10468
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").action(async (id, _options, command) => {
10469
+ const flags = getGlobalFlags(command);
10470
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10471
+ const params = {
10472
+ postId: id
10473
+ };
10474
+ const result = await client.call("GET", "/blog-posts/:postId", params, void 0, void 0);
10475
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10476
+ });
10477
+ blog.command("update").summary("Update a blog post").description(
10478
+ "Partial update of a blog post: title, slug (must be free, 409), Markdown body, excerpt, cover (null removes it), tags, SEO (whole section replaced). A published post keeps serving the new content at once. Returns the full post."
10479
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--title <value>", "title").option(
10480
+ "--slug <value>",
10481
+ "Public path segment (`/blog/{slug}`): lowercase letters, digits and hyphens."
10482
+ ).option("--content <value>", "The body as Markdown (headings, lists, links, images by URL).").option(
10483
+ "--excerpt <value>",
10484
+ "Short plain-text summary (list, RSS, meta description); derived from the body on publish when empty."
10485
+ ).option("--cover <value>", "Cover image URL; null removes the cover.").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option(
10486
+ "--seo <json|file>",
10487
+ "Replaces the whole SEO section.",
10488
+ parseJsonObjectFileOrInline("--seo")
10489
+ ).action(async (id, options, command) => {
10490
+ const flags = getGlobalFlags(command);
10491
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10492
+ const params = {
10493
+ postId: id
10494
+ };
10495
+ const payload = {
10496
+ title: options.title,
10497
+ slug: options.slug,
10498
+ content: options.content,
10499
+ excerpt: options.excerpt,
10500
+ coverImageUrl: options.cover,
10501
+ tags: options.tags,
10502
+ seo: options.seo
10503
+ };
10504
+ const result = await client.call("PATCH", "/blog-posts/:postId", params, void 0, payload);
10505
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10506
+ });
10507
+ blog.command("publish").summary("Publish or unpublish a blog post").description(
10508
+ "Publishes a post at `/blog/{slug}` (needs a non-empty body, 400 otherwise; the excerpt is derived from the body when empty) or takes it back to draft. The first publication date is the post's date and survives unpublish / republish. Reversible. Returns the full post."
10509
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--publish", "true = publish, false = back to draft.").action(async (id, options, command) => {
10510
+ const flags = getGlobalFlags(command);
10511
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10512
+ const params = {
10513
+ postId: id
10514
+ };
10515
+ const payload = {
10516
+ publish: options.publish
10517
+ };
10518
+ const result = await client.call(
10519
+ "POST",
10520
+ "/blog-posts/:postId/publish",
10521
+ params,
10522
+ void 0,
10523
+ payload
10524
+ );
10525
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10526
+ });
10527
+ blog.command("delete").summary("Delete a blog post").description(
10528
+ "Deletes a blog post for good; a published one disappears from `/blog` at once. No undo - prefer unpublishing."
10529
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--confirm", "required \u2014 acknowledge that you want to deletes the post for good").action(async (id, options, command) => {
10530
+ const flags = getGlobalFlags(command);
10531
+ requireConfirm(options.confirm, "deletes the post for good");
10532
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10533
+ const params = {
10534
+ postId: id
10535
+ };
10536
+ const result = await client.call(
10537
+ "DELETE",
10538
+ "/blog-posts/:postId",
10539
+ params,
10540
+ void 0,
10541
+ void 0
10542
+ );
10543
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10544
+ });
10545
+ }
10546
+
10225
10547
  // src/commands/_generated/index.ts
10226
10548
  function registerGeneratedCommands(program2) {
10227
10549
  registerCertificatesCommands(program2);
10550
+ registerBlogCommands(program2);
10228
10551
  }
10229
10552
 
10230
10553
  // src/commands/ads/index.ts
@@ -11787,10 +12110,10 @@ function registerBrandCommands(program2) {
11787
12110
  });
11788
12111
  }
11789
12112
  async function buildUpdatePatch(options) {
11790
- const { readFile: readFile3 } = await import("fs/promises");
12113
+ const { readFile: readFile4 } = await import("fs/promises");
11791
12114
  let patch = {};
11792
12115
  if (options.jsonFile) {
11793
- const raw = await readFile3(options.jsonFile, "utf8");
12116
+ const raw = await readFile4(options.jsonFile, "utf8");
11794
12117
  const parsed = JSON.parse(raw);
11795
12118
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
11796
12119
  throw new Error("--json-file must contain a JSON object with name/visual/editorial keys.");
@@ -11805,9 +12128,42 @@ async function buildUpdatePatch(options) {
11805
12128
  }
11806
12129
 
11807
12130
  // src/commands/bumps/index.ts
12131
+ var IMAGE_MEDIA_TYPE = "image";
12132
+ var CLEAR_MEDIA_VALUE = "none";
12133
+ var HTTP_URL_PATTERN = /^https?:\/\//i;
12134
+ var IMAGE_MIME_PREFIX = "image/";
12135
+ var JSON_PATCH_DESCRIPTION = "inline JSON object or .json path merged into the request body \u2014 for the deep `appearance` object (media {type, url, mediaId?}, backgroundColor, borderStyle, borderColor, borderWidth, ctaPosition, ctaSize, ctaBackgroundColor, ctaTextColor, ctaArrowPosition, ctaArrowColor, ctaArrowAnimation, imagePosition, imagePositionMobile, imageWidth). Explicit flags win over --json keys. Validated server-side.";
12136
+ var IMAGE_FLAG_DESCRIPTION = "image for the bump box: an https URL, a media library id (resolved via the media routes), or `none` to clear the media slot";
11808
12137
  function formatAmount(amountCents, currency) {
11809
12138
  return `${(amountCents / 100).toFixed(2)} ${currency}`;
11810
12139
  }
12140
+ function parseBumpJsonPatch(value) {
12141
+ return parseJsonObjectFileOrInline("--json")(value);
12142
+ }
12143
+ async function resolveImageMedia(client, value) {
12144
+ const trimmed = value.trim();
12145
+ if (trimmed.toLowerCase() === CLEAR_MEDIA_VALUE) return null;
12146
+ if (HTTP_URL_PATTERN.test(trimmed)) {
12147
+ return { type: IMAGE_MEDIA_TYPE, url: trimmed, mediaId: null };
12148
+ }
12149
+ const { media } = await client.getMedia(trimmed);
12150
+ if (!media.mimeType.startsWith(IMAGE_MIME_PREFIX)) {
12151
+ throw new Error(
12152
+ `--image: media ${trimmed} is ${media.mimeType}, not an image. Pass an image media id / URL, or set a video slot via --json {"appearance":{"media":{...}}}.`
12153
+ );
12154
+ }
12155
+ return { type: IMAGE_MEDIA_TYPE, url: media.url, mediaId: media.id };
12156
+ }
12157
+ function mergeBumpInput(jsonPatch, flagInput, media) {
12158
+ const merged = { ...jsonPatch, ...flagInput };
12159
+ for (const [key, value] of Object.entries(flagInput)) {
12160
+ if (value === void 0) delete merged[key];
12161
+ }
12162
+ const appearance = media !== void 0 ? { ...jsonPatch.appearance ?? {}, media } : jsonPatch.appearance;
12163
+ if (appearance !== void 0) merged.appearance = appearance;
12164
+ else delete merged.appearance;
12165
+ return merged;
12166
+ }
11811
12167
  function parsePriceIds(value) {
11812
12168
  const ids = value.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
11813
12169
  if (ids.length === 0) {
@@ -11834,39 +12190,52 @@ function registerBumpsCommands(program2) {
11834
12190
  );
11835
12191
  });
11836
12192
  });
11837
- bumps.command("create").description("Create an order bump from an existing product price (on its default offer)").requiredOption("--product <productId>", "product the bump adds").requiredOption("--price <priceId>", "price id on the product's default offer").requiredOption("--name <name>", "bump headline").option("--description <text>", "bump description").option("--cta <text>", "call-to-action text").action(
12193
+ bumps.command("create").description("Create an order bump from an existing product price (on its default offer)").requiredOption("--product <productId>", "product the bump adds").requiredOption("--price <priceId>", "price id on the product's default offer").requiredOption("--name <name>", "bump headline").option("--description <text>", "bump description").option("--cta <text>", "call-to-action text").option("--image <urlOrMediaId>", IMAGE_FLAG_DESCRIPTION).option("--json <fileOrJson>", JSON_PATCH_DESCRIPTION).action(
11838
12194
  async (options, command) => {
11839
12195
  const flags = getGlobalFlags(command);
11840
12196
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11841
- const result = await client.createOrderBump({
11842
- productId: options.product,
11843
- priceId: options.price,
11844
- name: options.name,
11845
- description: options.description,
11846
- ctaText: options.cta
11847
- });
12197
+ const jsonPatch = options.json !== void 0 ? parseBumpJsonPatch(options.json) : {};
12198
+ const media = options.image !== void 0 ? await resolveImageMedia(client, options.image) : void 0;
12199
+ const input = mergeBumpInput(
12200
+ jsonPatch,
12201
+ {
12202
+ productId: options.product,
12203
+ priceId: options.price,
12204
+ name: options.name,
12205
+ description: options.description,
12206
+ ctaText: options.cta
12207
+ },
12208
+ media
12209
+ );
12210
+ const result = await client.createOrderBump(input);
11848
12211
  emit(flags, result, () => {
11849
12212
  process.stdout.write(`Created order bump ${result.orderBumpId} \u2014 ${options.name}
11850
12213
  `);
11851
12214
  });
11852
12215
  }
11853
12216
  );
11854
- bumps.command("update").description("Update an order bump").argument("<bumpId>", "order bump id").option("--name <name>", "new headline").option("--description <text>", "new description").option("--cta <text>", "new call-to-action text").option("--product <productId>", "point the bump at a different product").option("--price <priceId>", "price id on the (new) product's default offer").action(
12217
+ bumps.command("update").description("Update an order bump").argument("<bumpId>", "order bump id").option("--name <name>", "new headline").option("--description <text>", "new description").option("--cta <text>", "new call-to-action text").option("--product <productId>", "point the bump at a different product").option("--price <priceId>", "price id on the (new) product's default offer").option("--image <urlOrMediaId>", IMAGE_FLAG_DESCRIPTION).option("--json <fileOrJson>", JSON_PATCH_DESCRIPTION).action(
11855
12218
  async (bumpId, options, command) => {
11856
12219
  const flags = getGlobalFlags(command);
11857
- const input = {
11858
- name: options.name,
11859
- description: options.description,
11860
- ctaText: options.cta,
11861
- productId: options.product,
11862
- priceId: options.price
11863
- };
11864
- if (Object.values(input).every((value) => value === void 0)) {
12220
+ const jsonPatch = options.json !== void 0 ? parseBumpJsonPatch(options.json) : {};
12221
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12222
+ const media = options.image !== void 0 ? await resolveImageMedia(client, options.image) : void 0;
12223
+ const input = mergeBumpInput(
12224
+ jsonPatch,
12225
+ {
12226
+ name: options.name,
12227
+ description: options.description,
12228
+ ctaText: options.cta,
12229
+ productId: options.product,
12230
+ priceId: options.price
12231
+ },
12232
+ media
12233
+ );
12234
+ if (Object.keys(input).length === 0) {
11865
12235
  throw new Error(
11866
- "Nothing to update \u2014 pass at least one of --name, --description, --cta, --product, --price."
12236
+ "Nothing to update \u2014 pass at least one of --name, --description, --cta, --product, --price, --image, or a --json patch."
11867
12237
  );
11868
12238
  }
11869
- const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11870
12239
  const result = await client.updateOrderBump(bumpId, input);
11871
12240
  emit(flags, result, () => {
11872
12241
  process.stdout.write(`Updated order bump ${result.orderBumpId}
@@ -11967,13 +12336,15 @@ function resolveSender(options) {
11967
12336
  };
11968
12337
  }
11969
12338
  function resolveTheme(options) {
11970
- if (options.backgroundColor === void 0 && options.contentBackgroundColor === void 0 && options.linkColor === void 0) {
12339
+ if (options.backgroundColor === void 0 && options.contentBackgroundColor === void 0 && options.linkColor === void 0 && options.buttonColor === void 0 && options.buttonTextColor === void 0) {
11971
12340
  return void 0;
11972
12341
  }
11973
12342
  return {
11974
12343
  ...options.backgroundColor !== void 0 ? { backgroundColor: options.backgroundColor } : {},
11975
12344
  ...options.contentBackgroundColor !== void 0 ? { contentBackgroundColor: options.contentBackgroundColor } : {},
11976
- ...options.linkColor !== void 0 ? { linkColor: options.linkColor } : {}
12345
+ ...options.linkColor !== void 0 ? { linkColor: options.linkColor } : {},
12346
+ ...options.buttonColor !== void 0 ? { buttonColor: options.buttonColor } : {},
12347
+ ...options.buttonTextColor !== void 0 ? { buttonTextColor: options.buttonTextColor } : {}
11977
12348
  };
11978
12349
  }
11979
12350
  function resolveAudience(options) {
@@ -12086,7 +12457,10 @@ function registerCampaignsCommands(program2) {
12086
12457
  ).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(
12087
12458
  "--from-email <email>",
12088
12459
  "custom From on the workspace's verified sending domain (requires --from-name); omit for the platform sender"
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(
12460
+ ).option("--segment <segmentId>", "send to a saved audience segment (re-resolved at send time)").option("--all-subscribed", "send to every subscribed client (the default audience)").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").option(
12461
+ "--button-color <hex>",
12462
+ "theme: background of buttons without their own color (wins over the look default)"
12463
+ ).option("--button-text-color <hex>", "theme: text color of buttons without their own color").action(
12090
12464
  async (options, command) => {
12091
12465
  const flags = getGlobalFlags(command);
12092
12466
  const contentMarkdown = resolveContent(options);
@@ -12114,7 +12488,10 @@ function registerCampaignsCommands(program2) {
12114
12488
  });
12115
12489
  }
12116
12490
  );
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(
12491
+ campaigns.command("update").description("Update a DRAFT campaign (campaigns past draft are immutable except cancel)").argument("<id>", "campaign id").option("--name <name>", "new internal label").option("--subject <subject>", "new email subject line").option("--preheader <text>", "new preview text").option("--content <markdown>", "new campaign body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new campaign body (Markdown) from a file").option("--html-file <path>", "read the new HTML document of a designed e-mail from a file").option("--template-id <id>", "look template for the body (`none` = the default shell)").option("--from-name <name>", "new From display name").option("--reply-to <email>", "new Reply-To address (requires --from-name)").option("--from-email <email>", "new custom From address (requires --from-name)").option("--segment <segmentId>", "switch the audience to a saved segment").option("--all-subscribed", "switch the audience to every subscribed client").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").option(
12492
+ "--button-color <hex>",
12493
+ "theme: background of buttons without their own color (wins over the look default)"
12494
+ ).option("--button-text-color <hex>", "theme: text color of buttons without their own color").action(
12118
12495
  async (id, options, command) => {
12119
12496
  const flags = getGlobalFlags(command);
12120
12497
  const input = {};
@@ -12201,6 +12578,28 @@ function registerCampaignsCommands(program2) {
12201
12578
  const { stats } = result.campaign;
12202
12579
  process.stdout.write(
12203
12580
  `Cancelled campaign ${result.campaign.id} \u2014 ${stats.sent} recipient${stats.sent === 1 ? " was" : "s were"} already sent to before the cancel.
12581
+ `
12582
+ );
12583
+ });
12584
+ });
12585
+ campaigns.command("resend").description(
12586
+ "Follow up a SENT campaign: builds a segment over its non-openers (or non-clickers) and creates a DRAFT copy aimed at it - nothing is sent; review and `campaigns send` the draft"
12587
+ ).argument("<id>", "campaign id (must be sent)").option("--target <target>", "not-opened (default) | not-clicked").action(async (id, options, command) => {
12588
+ const flags = getGlobalFlags(command);
12589
+ let target;
12590
+ if (options.target !== void 0) {
12591
+ if (options.target !== "not-opened" && options.target !== "not-clicked") {
12592
+ process.stderr.write("--target must be not-opened or not-clicked\n");
12593
+ process.exitCode = 1;
12594
+ return;
12595
+ }
12596
+ target = options.target;
12597
+ }
12598
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12599
+ const result = await client.resendEmailCampaign(id, target);
12600
+ emit(flags, result, () => {
12601
+ process.stdout.write(
12602
+ `Draft ${result.campaign.id} "${result.campaign.name}" created for segment "${result.segment.name}" (${result.segment.id}${result.segmentReused ? ", reused" : ""}). Nothing was sent - review it, then \`campaigns send <id> --confirm\`.
12204
12603
  `
12205
12604
  );
12206
12605
  });
@@ -12401,7 +12800,19 @@ function registerCheckoutSettingsCommands(program2) {
12401
12800
  ).option("--lang <language>", "language to write, e.g. 'pl' (defaults to 'pl')").option("--terms-url <url|none>", "terms & conditions link (none = clear)").option("--privacy-policy-url <url|none>", "privacy policy link (none = clear)").option(
12402
12801
  "--claim <text|none>",
12403
12802
  "information-clause / consent-checkbox claim text (none = clear)"
12404
- ).option("--terms-content <text>", "inline terms & conditions text").option("--privacy-policy-content <text>", "inline privacy policy text").option("--google-analytics-id <id|none>", "Google Analytics id (none = clear)").option("--facebook-pixel-id <id|none>", "Facebook Pixel id (none = clear)").option("--gtm-id <id|none>", "Google Tag Manager id (none = clear)").option("--tiktok-pixel-id <id|none>", "TikTok Pixel id (none = clear)").option("--promote-net-prices <on|off>", "display net prices prominently").option(
12803
+ ).option("--terms-content <text>", "inline terms & conditions text").option("--privacy-policy-content <text>", "inline privacy policy text").option(
12804
+ "--google-analytics-id <id|none>",
12805
+ "LEGACY Google Analytics id (none = clear) \u2014 prefer `integrations add ga4`"
12806
+ ).option(
12807
+ "--facebook-pixel-id <id|none>",
12808
+ "LEGACY Facebook Pixel id (none = clear) \u2014 Meta pixel is managed in the dashboard"
12809
+ ).option(
12810
+ "--gtm-id <id|none>",
12811
+ "LEGACY Google Tag Manager id (none = clear) \u2014 prefer `integrations add gtm`"
12812
+ ).option(
12813
+ "--tiktok-pixel-id <id|none>",
12814
+ "LEGACY TikTok Pixel id (none = clear) \u2014 prefer `integrations add tiktok`"
12815
+ ).option("--promote-net-prices <on|off>", "display net prices prominently").option(
12405
12816
  "--price-variants-placement <belowDescription|aboveDescription|none>",
12406
12817
  "where the price list renders relative to the description"
12407
12818
  ).option(
@@ -12536,6 +12947,39 @@ var AIImageQuality = /* @__PURE__ */ ((AIImageQuality2) => {
12536
12947
  AIImageQuality2["Ultra"] = "4K";
12537
12948
  return AIImageQuality2;
12538
12949
  })(AIImageQuality || {});
12950
+ var MANAGED_CONVERSION_TRACKING_CLIENTS = [
12951
+ "googleAnalytics",
12952
+ "googleTagManager",
12953
+ "tiktok",
12954
+ "x",
12955
+ "linkedin",
12956
+ "microsoftClarity",
12957
+ "hotjar"
12958
+ ];
12959
+ var CONNECTABLE_EMAIL_CLIENTS = [
12960
+ "ActiveCampaign",
12961
+ "ConvertKit",
12962
+ "MailerLite",
12963
+ "MailChimp",
12964
+ "GetResponse",
12965
+ "Pipedrive"
12966
+ ];
12967
+ var INVOICES_CLIENTS = [
12968
+ "fakturownia",
12969
+ "ifirma",
12970
+ "inFakt",
12971
+ "wFirmaV2",
12972
+ "ingKsiegowosc",
12973
+ "stripe"
12974
+ ];
12975
+ var CREDENTIAL_PAYMENT_GATEWAYS = [
12976
+ "payu",
12977
+ "tpay",
12978
+ "przelewy24",
12979
+ "paynow",
12980
+ "paypal",
12981
+ "dodo"
12982
+ ];
12539
12983
  var COMMUNITY_EVENT_LOCATION_TYPES = [
12540
12984
  "online",
12541
12985
  "inPerson"
@@ -13618,6 +14062,7 @@ Next page: --cursor ${page.nextCursor}
13618
14062
  let created = 0;
13619
14063
  let existing = 0;
13620
14064
  let invalid = 0;
14065
+ let overLimit = 0;
13621
14066
  for (let offset = 0; offset < contacts.length; offset += IMPORT_CHUNK_SIZE) {
13622
14067
  const chunk2 = contacts.slice(offset, offset + IMPORT_CHUNK_SIZE);
13623
14068
  if (!flags.json && contacts.length > IMPORT_CHUNK_SIZE) {
@@ -13631,13 +14076,18 @@ Next page: --cursor ${page.nextCursor}
13631
14076
  created += page.created;
13632
14077
  existing += page.existing;
13633
14078
  invalid += page.invalid;
14079
+ overLimit += page.overLimit ?? 0;
13634
14080
  }
13635
- const summary = { results, created, existing, invalid };
14081
+ const summary = { results, created, existing, invalid, overLimit };
13636
14082
  emit(flags, summary, () => {
13637
14083
  process.stdout.write(
13638
- `Imported ${contacts.length} row${contacts.length === 1 ? "" : "s"} from ${csvFile}: ${created} created, ${existing} already existed, ${invalid} invalid.
13639
- `
14084
+ `Imported ${contacts.length} row${contacts.length === 1 ? "" : "s"} from ${csvFile}: ${created} created, ${existing} already existed, ${invalid} invalid` + (overLimit > 0 ? `, ${overLimit} over the plan's contacts limit` : "") + ".\n"
13640
14085
  );
14086
+ if (overLimit > 0) {
14087
+ process.stdout.write(
14088
+ "The plan's contacts limit is reached \u2014 upgrade the plan to import the remaining rows.\n"
14089
+ );
14090
+ }
13641
14091
  const problems = results.filter((r) => r.status !== "created" && r.status !== "exists");
13642
14092
  if (problems.length > 0) {
13643
14093
  process.stdout.write("\nRows that were not imported:\n");
@@ -14727,11 +15177,11 @@ Next page: --cursor ${result.nextCursor}
14727
15177
  member.command("mute").description("Mute (timeout) a member for N minutes (1..40320 = 28 days)").argument("<communityId>", "community id").argument("<memberId>", "member id (wcid)").requiredOption("--minutes <n>", "mute duration in minutes (1..40320)").action(
14728
15178
  async (communityId, memberId, options, command) => {
14729
15179
  const flags = getGlobalFlags(command);
14730
- const minutes = Number.parseInt(options.minutes, 10);
14731
- if (!Number.isInteger(minutes) || minutes < 1 || minutes > 40320) {
15180
+ const minutes2 = Number.parseInt(options.minutes, 10);
15181
+ if (!Number.isInteger(minutes2) || minutes2 < 1 || minutes2 > 40320) {
14732
15182
  throw new Error(`Invalid --minutes: ${options.minutes} (expected an integer 1..40320).`);
14733
15183
  }
14734
- const result = await clientFor(flags).muteCommunityMember(communityId, memberId, minutes);
15184
+ const result = await clientFor(flags).muteCommunityMember(communityId, memberId, minutes2);
14735
15185
  emit(flags, result, () => {
14736
15186
  process.stdout.write(`Muted member ${memberId} until ${result.mutedUntil}.
14737
15187
  `);
@@ -17067,6 +17517,10 @@ function printSignupFormDetail(form) {
17067
17517
  `);
17068
17518
  process.stdout.write(` Redirect: ${form.redirectUrl ?? "(success state on the form)"}
17069
17519
  `);
17520
+ process.stdout.write(
17521
+ ` After confirmation: ${form.confirmationRedirectUrl ?? "(platform confirmation page)"}
17522
+ `
17523
+ );
17070
17524
  if (form.confirmationEmail) {
17071
17525
  process.stdout.write(
17072
17526
  ` Confirmation email overrides: subject ${form.confirmationEmail.subject ?? "(default)"}, intro ${form.confirmationEmail.intro ?? "(default)"}
@@ -17086,6 +17540,36 @@ function printSignupFormDetail(form) {
17086
17540
  `);
17087
17541
  }
17088
17542
  }
17543
+ var escapeHtmlAttribute = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
17544
+ function buildFormSnippet(input) {
17545
+ const copy = input.lang === "pl" ? {
17546
+ email: "Adres e-mail",
17547
+ firstName: "Imi\u0119",
17548
+ submit: "Zapisuj\u0119 si\u0119",
17549
+ success: "Dzi\u0119kujemy! Sprawd\u017A skrzynk\u0119 i potwierd\u017A zapis.",
17550
+ error: "Nie uda\u0142o si\u0119 wys\u0142a\u0107. Spr\xF3buj ponownie."
17551
+ } : {
17552
+ email: "E-mail address",
17553
+ firstName: "First name",
17554
+ submit: "Sign me up",
17555
+ success: "Thank you! Check your inbox to confirm.",
17556
+ error: "Something went wrong. Please try again."
17557
+ };
17558
+ const redirect = input.redirectUrl ? ` data-zanfia-redirect="${escapeHtmlAttribute(input.redirectUrl)}"` : "";
17559
+ const firstName = input.firstName ? `
17560
+ <label>${copy.firstName}<br><input name="firstName" type="text" autocomplete="given-name"></label>` : "";
17561
+ return [
17562
+ `<form data-zanfia-form="${input.workspaceId}/${input.formId}"${redirect}>`,
17563
+ ` <label>${copy.email}<br><input name="email" type="email" required autocomplete="email"></label>${firstName}`,
17564
+ ` <input name="website" type="text" tabindex="-1" autocomplete="off" aria-hidden="true" style="position:absolute;left:-9999px;opacity:0">`,
17565
+ ` <button type="submit">${copy.submit}</button>`,
17566
+ ` <div data-zanfia-success hidden>${copy.success}</div>`,
17567
+ ` <div data-zanfia-error hidden>${copy.error}</div>`,
17568
+ `</form>`,
17569
+ `<script src="${input.platformUrl}/zanfia-page-forms.js" defer></script>`,
17570
+ ""
17571
+ ].join("\n");
17572
+ }
17089
17573
  async function buildFormUpdateInput(client, formId, options) {
17090
17574
  const input = options.json !== void 0 ? parseFormJsonPatch(options.json) : {};
17091
17575
  if (options.name !== void 0) input.name = options.name;
@@ -17094,6 +17578,9 @@ async function buildFormUpdateInput(client, formId, options) {
17094
17578
  input.optInMode = parseChoice2(options.optIn, OPT_IN_MODES, "--opt-in");
17095
17579
  }
17096
17580
  if (options.redirect !== void 0) input.redirectUrl = orNull3(options.redirect);
17581
+ if (options.confirmationRedirect !== void 0) {
17582
+ input.confirmationRedirectUrl = orNull3(options.confirmationRedirect);
17583
+ }
17097
17584
  let current;
17098
17585
  const fetchCurrent = async () => {
17099
17586
  current ??= await client.getSignupForm(formId);
@@ -17156,7 +17643,10 @@ function registerFormsCommands(program2) {
17156
17643
  });
17157
17644
  forms.command("create").description(
17158
17645
  "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(
17646
+ ).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(
17647
+ "--confirmation-redirect <url>",
17648
+ "where the confirmation e-mail click lands (double opt-in): lead magnet, OTO, calendar"
17649
+ ).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
17650
  "--sms-consent <on|off>",
17161
17651
  "config: marketing-SMS consent checkbox (needs the phone field)"
17162
17652
  ).option(
@@ -17174,7 +17664,8 @@ function registerFormsCommands(program2) {
17174
17664
  tagIds: options.tags !== void 0 ? parseCsv2(options.tags) : void 0,
17175
17665
  config,
17176
17666
  ...optInMode !== void 0 ? { optInMode } : {},
17177
- ...options.redirect !== void 0 ? { redirectUrl: options.redirect } : {}
17667
+ ...options.redirect !== void 0 ? { redirectUrl: options.redirect } : {},
17668
+ ...options.confirmationRedirect !== void 0 ? { confirmationRedirectUrl: options.confirmationRedirect } : {}
17178
17669
  });
17179
17670
  emit(flags, result, () => {
17180
17671
  process.stdout.write(
@@ -17186,7 +17677,10 @@ function registerFormsCommands(program2) {
17186
17677
  );
17187
17678
  forms.command("update").description(
17188
17679
  "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."
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(
17680
+ ).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(
17681
+ "--confirmation-redirect <url|none>",
17682
+ "where the confirmation e-mail click lands (double opt-in); `none` restores the platform page"
17683
+ ).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
17684
  "--sms-consent <on|off>",
17191
17685
  "config: marketing-SMS consent checkbox (needs the phone field)"
17192
17686
  ).option(
@@ -17221,6 +17715,45 @@ function registerFormsCommands(program2) {
17221
17715
  `);
17222
17716
  });
17223
17717
  });
17718
+ forms.command("snippet").description(
17719
+ "Print the HTML of a native signup form for ANY website (WordPress, Webflow, plain HTML): the form markup + the platform script. Paste it into the page; tags, opt-in mode and the thank-you redirect come from the form settings. Edit the markup freely, keeping the field names and the data-zanfia-* hooks."
17720
+ ).argument("<id>", "form id").option("--lang <pl|en>", "language of the placeholder copy (default pl)").option("--first-name", "include a first-name field").option(
17721
+ "--redirect <url>",
17722
+ "thank-you URL for THIS embed (overrides the form setting on the page)"
17723
+ ).option(
17724
+ "--platform-url <origin>",
17725
+ "origin that serves zanfia-page-forms.js (default: this build\u2019s platform, or $ZANFIA_PLATFORM_URL)"
17726
+ ).action(
17727
+ async (id, options, command) => {
17728
+ const flags = getGlobalFlags(command);
17729
+ const lang = options.lang !== void 0 ? parseChoice2(options.lang, ["pl", "en"], "--lang") : "pl";
17730
+ if (options.redirect !== void 0 && !/^https?:\/\/\S+$/.test(options.redirect)) {
17731
+ throw new Error("--redirect must be an absolute http(s) URL.");
17732
+ }
17733
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
17734
+ const [form, workspace2] = await Promise.all([
17735
+ client.getSignupForm(id),
17736
+ client.getWorkspaceSettings()
17737
+ ]);
17738
+ if (form.submitAction === "freeProductSignup") {
17739
+ throw new Error(
17740
+ `Form ${id} is product-bound; the native snippet works for standalone signup forms only.`
17741
+ );
17742
+ }
17743
+ const platformUrl = (options.platformUrl ?? resolvePlatformUrl()).replace(/\/+$/, "");
17744
+ const snippet = buildFormSnippet({
17745
+ workspaceId: workspace2.workspaceId,
17746
+ formId: form.id,
17747
+ platformUrl,
17748
+ lang,
17749
+ redirectUrl: options.redirect ?? form.redirectUrl ?? null,
17750
+ firstName: options.firstName === true
17751
+ });
17752
+ emit(flags, { formId: form.id, workspaceId: workspace2.workspaceId, html: snippet }, () => {
17753
+ process.stdout.write(snippet);
17754
+ });
17755
+ }
17756
+ );
17224
17757
  forms.command("submissions").description("List a form's submissions (newest first)").argument("<id>", "form id").option("--limit <n>", "maximum rows to return").action(async (id, options, command) => {
17225
17758
  const flags = getGlobalFlags(command);
17226
17759
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -17258,6 +17791,7 @@ var FUNNEL_TEMPLATE_IDS = [
17258
17791
  ];
17259
17792
  var EVENT_MODES = ["live", "on_demand"];
17260
17793
  var SCHEDULER_UNAVAILABLE_STATUS2 = 503;
17794
+ var FUNNEL_NOT_READY_STATUS = 409;
17261
17795
  function requireConfirm6(confirm, what) {
17262
17796
  if (!confirm) {
17263
17797
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
@@ -17273,6 +17807,16 @@ function withSchedulerHint2(error) {
17273
17807
  }
17274
17808
  throw error;
17275
17809
  }
17810
+ function withLiveGateHint(error, funnelId) {
17811
+ if (error instanceof ApiError && error.status === FUNNEL_NOT_READY_STATUS) {
17812
+ throw new ApiError(
17813
+ error.status,
17814
+ error.apiMessage,
17815
+ `The funnel was not changed. Run \`funnels get ${funnelId}\` to see each step; publish the page, activate the checkout, or generate the entry page (\`funnels generate-page\`), then retry.`
17816
+ );
17817
+ }
17818
+ throw error;
17819
+ }
17276
17820
  function parsePositiveInt10(value, flag) {
17277
17821
  const n = Number.parseInt(value, 10);
17278
17822
  if (!Number.isInteger(n) || n <= 0) {
@@ -17449,19 +17993,19 @@ function registerFunnelsCommands(program2) {
17449
17993
  const flags = getGlobalFlags(command);
17450
17994
  const input = buildFunnelUpdateInput(options);
17451
17995
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
17452
- const result = await client.updateFunnel(id, input);
17996
+ const result = await client.updateFunnel(id, input).catch((error) => withLiveGateHint(error, id));
17453
17997
  emit(flags, result, () => {
17454
17998
  process.stdout.write(`Updated funnel ${id}.
17455
17999
  `);
17456
18000
  });
17457
18001
  });
17458
18002
  funnels.command("status").description(
17459
- "Change a funnel's status: draft (building), live (running \u2014 the entry step link is meant to be shared), archived (retired \u2014 the reversible substitute for deletion). All transitions are reversible."
18003
+ "Change a funnel's status: draft (building), live (running \u2014 the entry step link is meant to be shared), archived (retired \u2014 the reversible substitute for deletion). Going live is refused (409 funnel-not-ready) while the entry step is not ready or a referenced page is unpublished / checkout inactive. All transitions are reversible."
17460
18004
  ).argument("<id>", "funnel id").requiredOption("--set <status>", `new status (${FUNNEL_STATUSES.join(" | ")})`).action(async (id, options, command) => {
17461
18005
  const flags = getGlobalFlags(command);
17462
18006
  const status = parseChoice3(options.set, FUNNEL_STATUSES, "--set");
17463
18007
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
17464
- const result = await client.updateFunnel(id, { status });
18008
+ const result = await client.updateFunnel(id, { status }).catch((error) => withLiveGateHint(error, id));
17465
18009
  emit(flags, result, () => {
17466
18010
  process.stdout.write(`Funnel ${id} status set to ${status}.
17467
18011
  `);
@@ -17530,7 +18074,10 @@ function registerFunnelsCommands(program2) {
17530
18074
  });
17531
18075
  funnels.command("from-template").description(
17532
18076
  "Create a funnel from a catalog template (`funnels templates`) AND AI-generate its page steps in one go \u2014 opt-in / sales / thank-you pages are queued as background generations (202; consumes AI credits, 1-5 minutes each). The template\u2019s product requirement must be met (free or paid checkout on the product), else 409. Webinar template: pass the event fields (live) or --event-mode on_demand. Requires --confirm."
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(
18077
+ ).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(
18078
+ "--page-template <id>",
18079
+ "design template for every generated page (a page-template id, e.g. course-fresh); thank-you steps take the companion"
18080
+ ).option("--confirm", "required \u2014 acknowledge that AI credits will be consumed").action(
17534
18081
  async (templateId, options, command) => {
17535
18082
  const flags = getGlobalFlags(command);
17536
18083
  requireConfirm6(options.confirm, "create the funnel and generate its pages (AI credits)");
@@ -17538,7 +18085,8 @@ function registerFunnelsCommands(program2) {
17538
18085
  templateId: parseChoice3(templateId, FUNNEL_TEMPLATE_IDS, "<templateId>"),
17539
18086
  productId: options.product,
17540
18087
  lang: parseChoice3(options.lang, FUNNEL_LANGS, "--lang"),
17541
- name: options.name
18088
+ name: options.name,
18089
+ ...options.pageTemplate ? { pageTemplateId: options.pageTemplate } : {}
17542
18090
  };
17543
18091
  const webinar = {
17544
18092
  eventStartAt: options.eventStart,
@@ -17651,42 +18199,968 @@ function registerFunnelsCommands(program2) {
17651
18199
  });
17652
18200
  }
17653
18201
 
17654
- // src/commands/join/index.ts
17655
- var MAX_CODE_ATTEMPTS2 = 5;
17656
- var HANDLE_REGEX = /^[a-z0-9][a-z0-9-]{1,62}$/;
17657
- var EMAIL_REGEX2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
17658
- var AGENT_DOCS_URL = "https://zanfia.com/help/developer-cli/agents";
17659
- function joinUrl(base, handle, step) {
17660
- return `${base.replace(/\/$/, "")}/v1/join/${encodeURIComponent(handle)}/${step}`;
17661
- }
17662
- async function runJoin(handleInput, options) {
17663
- const handle = handleInput.trim().toLowerCase();
17664
- if (!HANDLE_REGEX.test(handle)) {
18202
+ // src/commands/integrations/connections.ts
18203
+ import { readFileSync as readFileSync15 } from "fs";
18204
+ var EMAIL_CLIENT_ALIASES = {
18205
+ ac: "ActiveCampaign",
18206
+ activecampaign: "ActiveCampaign",
18207
+ kit: "ConvertKit",
18208
+ convertkit: "ConvertKit",
18209
+ mailerlite: "MailerLite",
18210
+ mailchimp: "MailChimp",
18211
+ getresponse: "GetResponse",
18212
+ freshmail: "FreshMail",
18213
+ pipedrive: "Pipedrive"
18214
+ };
18215
+ function parseEmailClient(value) {
18216
+ const match = EMAIL_CLIENT_ALIASES[value.trim().toLowerCase()];
18217
+ if (!match) {
17665
18218
  throw new Error(
17666
- "Community handle must be lowercase letters, digits and dashes (e.g. crew-of-one)."
18219
+ `Unknown email client: ${value} (expected ${CONNECTABLE_EMAIL_CLIENTS.join(" | ")}; FreshMail is disconnect-only)`
17667
18220
  );
17668
18221
  }
17669
- const communityApiUrl = resolveCommunityApiUrl();
17670
- if (!communityApiUrl) {
17671
- throw new Error(
17672
- "No Community API URL configured. Set ZANFIA_COMMUNITY_API_URL (this build has no baked backend)."
17673
- );
18222
+ return match;
18223
+ }
18224
+ function parseOnOff2(value, flag) {
18225
+ if (value === void 0) return void 0;
18226
+ if (value === "on") return true;
18227
+ if (value === "off") return false;
18228
+ throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
18229
+ }
18230
+ function requireConfirm7(confirm, what) {
18231
+ if (!confirm) {
18232
+ throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
17674
18233
  }
17675
- const verbose = options.verbose === true;
17676
- const email = (options.email ?? await promptLine("Email: ")).trim().toLowerCase();
17677
- if (!EMAIL_REGEX2.test(email)) {
17678
- throw new Error("Please enter a valid email address.");
18234
+ }
18235
+ function readSecret(label, value, fromStdin, fromEnv) {
18236
+ const sources = [value !== void 0, fromStdin === true, fromEnv !== void 0].filter(Boolean);
18237
+ if (sources.length > 1) {
18238
+ throw new Error(`Pass ${label} through exactly one of the value, -stdin or -env forms.`);
17679
18239
  }
17680
- const started = await postJson2(
17681
- "Join",
17682
- joinUrl(communityApiUrl, handle, "start"),
17683
- { email, lang: "en" },
17684
- { verbose }
17685
- );
17686
- if (!options.json) {
17687
- process.stdout.write(
17688
- `Joining ${started.communityName} (${started.workspaceName}) \u2014 a 6-digit code was sent to ${email}.
17689
- `
18240
+ if (fromStdin) {
18241
+ const raw = readFileSync15(0, "utf8").trim();
18242
+ if (!raw) throw new Error(`${label}: nothing was read from stdin.`);
18243
+ return raw;
18244
+ }
18245
+ if (fromEnv !== void 0) {
18246
+ const raw = process.env[fromEnv]?.trim();
18247
+ if (!raw) throw new Error(`${label}: environment variable ${fromEnv} is empty or unset.`);
18248
+ return raw;
18249
+ }
18250
+ return value;
18251
+ }
18252
+ function printOverview(overview) {
18253
+ process.stdout.write("Email clients\n");
18254
+ if (overview.emailClients.length === 0) {
18255
+ process.stdout.write(" (none connected)\n");
18256
+ } else {
18257
+ printTable(
18258
+ overview.emailClients.map((account) => ({
18259
+ client: account.client,
18260
+ url: account.url,
18261
+ lists: account.listsCount,
18262
+ tags: account.tagsCount,
18263
+ updatedAt: account.updatedAt ?? account.createdAt
18264
+ })),
18265
+ ["client", "url", "lists", "tags", "updatedAt"]
18266
+ );
18267
+ }
18268
+ process.stdout.write("Meta pixel / Conversions API\n");
18269
+ if (overview.metaConversionApi.length === 0) {
18270
+ process.stdout.write(" (none)\n");
18271
+ } else {
18272
+ printMetaEntries(overview.metaConversionApi);
18273
+ }
18274
+ process.stdout.write(
18275
+ `Managed pixel integrations (GA4 / GTM / \u2026): ${overview.conversionTrackingIntegrationsCount} \u2014 see \`integrations list\`
18276
+ `
18277
+ );
18278
+ process.stdout.write("Invoicing\n");
18279
+ if (overview.invoicesClients.length === 0) {
18280
+ process.stdout.write(" (none connected)\n");
18281
+ } else {
18282
+ printTable(
18283
+ overview.invoicesClients.map((account) => ({
18284
+ client: account.client,
18285
+ id: account.id,
18286
+ url: account.url
18287
+ })),
18288
+ ["client", "id", "url"]
18289
+ );
18290
+ }
18291
+ const p = overview.payments;
18292
+ const on = (value) => value ? "on" : "off";
18293
+ process.stdout.write(
18294
+ `Payments: stripe=${on(p.stripe)} payU=${on(p.payU)} tpay=${on(p.tpay)} payNow=${on(p.payNow)} przelewy24=${on(p.przelewy24)} paypal=${on(p.paypal)} dodo=${on(p.dodo)} manual=${p.manual ? `on (${p.manual.name})` : "off"}
18295
+ `
18296
+ );
18297
+ }
18298
+ function printMetaEntries(entries) {
18299
+ printTable(
18300
+ entries.map((entry) => ({
18301
+ id: entry.id,
18302
+ name: entry.name,
18303
+ pixelId: entry.pixelId,
18304
+ serverSide: entry.serverSide ? "yes" : "",
18305
+ default: entry.isWorkspaceDefault ? "yes" : "",
18306
+ disabled: entry.disabled ? "yes" : "",
18307
+ events: [
18308
+ entry.trackPurchase ? "purchase" : null,
18309
+ entry.trackInitCheckout ? "initCheckout" : null,
18310
+ entry.trackAddToCart ? "addToCart" : null,
18311
+ entry.trackTrialStarted ? "trialStarted" : null
18312
+ ].filter(Boolean).join(",")
18313
+ })),
18314
+ ["id", "name", "pixelId", "serverSide", "default", "disabled", "events"]
18315
+ );
18316
+ }
18317
+ function printAccount(account, headline) {
18318
+ process.stdout.write(`${headline} ${account.client} ${account.url}
18319
+ `);
18320
+ process.stdout.write(` Lists: ${account.listsCount} Tags: ${account.tagsCount}
18321
+ `);
18322
+ }
18323
+ function buildEmailConnectInput(client, options) {
18324
+ if (client === "FreshMail") {
18325
+ throw new Error(
18326
+ "FreshMail can no longer be connected (provider retired); existing accounts can only be disconnected."
18327
+ );
18328
+ }
18329
+ const secret = readSecret("--api-key", options.apiKey, options.apiKeyStdin, options.apiKeyEnv);
18330
+ if (!secret) throw new Error("--api-key (or --api-key-stdin / --api-key-env VAR) is required.");
18331
+ const input = { client, url: options.url };
18332
+ switch (client) {
18333
+ case "ConvertKit":
18334
+ input.apiSecret = secret;
18335
+ break;
18336
+ case "Pipedrive":
18337
+ input.apiSecret = secret;
18338
+ if (!options.tagsField || !options.listsField) {
18339
+ throw new Error("Pipedrive needs --tags-field and --lists-field (person field names).");
18340
+ }
18341
+ input.tagsFieldName = options.tagsField;
18342
+ input.listsFieldName = options.listsField;
18343
+ break;
18344
+ case "MailChimp":
18345
+ input.apiKey = secret;
18346
+ if (!options.serverPrefix) throw new Error("MailChimp needs --server-prefix (e.g. us21).");
18347
+ input.serverPrefix = options.serverPrefix;
18348
+ break;
18349
+ default:
18350
+ input.apiKey = secret;
18351
+ }
18352
+ return input;
18353
+ }
18354
+ function buildMetaInput(options, existing) {
18355
+ const name = options.name ?? existing?.name;
18356
+ const pixelId = options.pixelId ?? existing?.pixelId;
18357
+ if (!name) throw new Error("--name is required.");
18358
+ if (!pixelId) throw new Error("--pixel-id is required.");
18359
+ const token = readSecret(
18360
+ "--access-token",
18361
+ options.accessToken,
18362
+ options.accessTokenStdin,
18363
+ options.accessTokenEnv
18364
+ );
18365
+ return {
18366
+ ...existing ? { id: existing.id } : {},
18367
+ name,
18368
+ pixelId,
18369
+ ...token !== void 0 ? { accessToken: token } : {},
18370
+ trackPurchase: parseOnOff2(options.trackPurchase, "--track-purchase") ?? existing?.trackPurchase ?? true,
18371
+ trackInitCheckout: parseOnOff2(options.trackInitCheckout, "--track-init-checkout") ?? existing?.trackInitCheckout ?? true,
18372
+ trackAddToCart: parseOnOff2(options.trackAddToCart, "--track-add-to-cart") ?? existing?.trackAddToCart ?? true,
18373
+ trackTrialStarted: parseOnOff2(options.trackTrialStarted, "--track-trial-started") ?? existing?.trackTrialStarted ?? false
18374
+ };
18375
+ }
18376
+ function addMetaOptions(command) {
18377
+ return command.option("--name <name>", "creator-facing label").option("--pixel-id <id>", "Meta pixel / dataset id").option(
18378
+ "--access-token <token>",
18379
+ "Conversions API access token (prefer --access-token-stdin / -env)"
18380
+ ).option("--access-token-stdin", "read the access token from stdin").option("--access-token-env <VAR>", "read the access token from an environment variable").option("--track-purchase <on|off>", "send Purchase (default on)").option("--track-init-checkout <on|off>", "send InitiateCheckout (default on)").option("--track-add-to-cart <on|off>", "send AddToCart (default on)").option("--track-trial-started <on|off>", "send TrialStarted (default off)");
18381
+ }
18382
+ function registerIntegrationConnectionCommands(integrations) {
18383
+ integrations.command("status").description(
18384
+ "What is connected: email clients, Meta pixel / Conversions API entries, invoicing, payment methods (needs integrations:read)"
18385
+ ).action(async (_options, command) => {
18386
+ const flags = getGlobalFlags(command);
18387
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18388
+ const overview = await client.getIntegrationsOverview();
18389
+ emit(flags, overview, () => printOverview(overview));
18390
+ });
18391
+ const emailClients = integrations.command("email-clients").description(
18392
+ "Email-marketing accounts: ActiveCampaign, Kit (ConvertKit), MailerLite, MailChimp, GetResponse, Pipedrive. Keys are verified against the provider before saving and never printed."
18393
+ );
18394
+ emailClients.command("connect <client>").description(
18395
+ "Connect (or re-connect, same url) an account. Client: ac | kit | mailerlite | mailchimp | getresponse | pipedrive. Pass the key via --api-key-stdin or --api-key-env to keep it out of shell history."
18396
+ ).requiredOption("--url <url>", "account / API base URL as the provider shows it").option("--api-key <key>", "API key (Kit / Pipedrive: the API secret)").option("--api-key-stdin", "read the key from stdin").option("--api-key-env <VAR>", "read the key from an environment variable").option("--server-prefix <prefix>", "MailChimp data-center prefix, e.g. us21").option("--tags-field <name>", "Pipedrive: person field used as tags").option("--lists-field <name>", "Pipedrive: person field used as lists").action(async (clientArg, options, command) => {
18397
+ const flags = getGlobalFlags(command);
18398
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18399
+ const input = buildEmailConnectInput(parseEmailClient(clientArg), options);
18400
+ const account = await client.connectEmailClient(input);
18401
+ emit(flags, account, () => printAccount(account, "Connected"));
18402
+ });
18403
+ emailClients.command("disconnect <client>").description(
18404
+ "Disconnect an account by url (irreversible, the stored key is deleted). Refused while a live newsletter / automation still uses it. Requires --confirm."
18405
+ ).requiredOption("--url <url>", "account url from `integrations status`").option("--confirm", "required \u2014 acknowledges the key is deleted").action(
18406
+ async (clientArg, options, command) => {
18407
+ requireConfirm7(options.confirm, `disconnect ${clientArg} ${options.url}`);
18408
+ const flags = getGlobalFlags(command);
18409
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18410
+ const parsed = parseEmailClient(clientArg);
18411
+ const result = await client.disconnectEmailClient(parsed, options.url);
18412
+ emit(flags, { ...result, client: parsed, url: options.url }, () => {
18413
+ process.stdout.write(`Disconnected ${parsed} ${options.url}
18414
+ `);
18415
+ });
18416
+ }
18417
+ );
18418
+ emailClients.command("refresh <client>").description(
18419
+ "Re-fetch the lists + tags of a connected account with the stored key (not Pipedrive)"
18420
+ ).requiredOption("--url <url>", "account url from `integrations status`").option("--server-prefix <prefix>", "MailChimp data-center prefix").action(
18421
+ async (clientArg, options, command) => {
18422
+ const flags = getGlobalFlags(command);
18423
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18424
+ const parsed = parseEmailClient(clientArg);
18425
+ if (parsed === "Pipedrive" || parsed === "FreshMail") {
18426
+ throw new Error(`${parsed} lists cannot be refreshed from the CLI.`);
18427
+ }
18428
+ const account = await client.refreshEmailClientLists({
18429
+ client: parsed,
18430
+ url: options.url,
18431
+ ...options.serverPrefix ? { serverPrefix: options.serverPrefix } : {}
18432
+ });
18433
+ emit(flags, account, () => printAccount(account, "Refreshed"));
18434
+ }
18435
+ );
18436
+ const meta = integrations.command("meta-capi").description(
18437
+ "Manual Meta pixel + Conversions API entries (the connect-with-Facebook OAuth path stays in the dashboard)"
18438
+ );
18439
+ meta.command("list").description("Meta pixel / Conversions API entries").action(async (_options, command) => {
18440
+ const flags = getGlobalFlags(command);
18441
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18442
+ const overview = await client.getIntegrationsOverview();
18443
+ emit(flags, overview.metaConversionApi, () => printMetaEntries(overview.metaConversionApi));
18444
+ });
18445
+ addMetaOptions(
18446
+ meta.command("add").description(
18447
+ "Add a Meta pixel entry; with an access token the Conversions API (server-side events) is enabled \u2014 the token is validated against Meta before saving. The first tracking integration a workspace connects becomes the workspace default."
18448
+ )
18449
+ ).action(async (options, command) => {
18450
+ const flags = getGlobalFlags(command);
18451
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18452
+ const entry = await client.connectMetaConversionApi(buildMetaInput(options, null));
18453
+ emit(flags, entry, () => printMetaEntries([entry]));
18454
+ });
18455
+ addMetaOptions(
18456
+ meta.command("update <integrationId>").description("Edit an entry \u2014 only the flags you pass change; an omitted token stays stored")
18457
+ ).action(async (integrationId, options, command) => {
18458
+ const flags = getGlobalFlags(command);
18459
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18460
+ const overview = await client.getIntegrationsOverview();
18461
+ const existing = overview.metaConversionApi.find((entry2) => entry2.id === integrationId);
18462
+ if (!existing) {
18463
+ throw new Error(
18464
+ `No Meta Conversions API entry with id ${integrationId}. Run \`zanfia integrations meta-capi list\`.`
18465
+ );
18466
+ }
18467
+ const entry = await client.connectMetaConversionApi(buildMetaInput(options, existing));
18468
+ emit(flags, entry, () => printMetaEntries([entry]));
18469
+ });
18470
+ meta.command("remove <integrationId>").description("Remove an entry (irreversible). Requires --confirm.").option("--confirm", "required \u2014 acknowledges the entry is deleted").action(async (integrationId, options, command) => {
18471
+ requireConfirm7(options.confirm, `remove Meta entry ${integrationId}`);
18472
+ const flags = getGlobalFlags(command);
18473
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18474
+ const result = await client.disconnectMetaConversionApi(integrationId);
18475
+ emit(flags, { ...result, integrationId }, () => {
18476
+ process.stdout.write(`Removed Meta entry ${integrationId}
18477
+ `);
18478
+ });
18479
+ });
18480
+ const manual = integrations.command("manual-payment").description(
18481
+ "The manual / wire-transfer payment method (buyers pay by transfer, you approve the order)"
18482
+ );
18483
+ manual.command("set").description("Configure (create or update) the manual payment method").requiredOption("--name <name>", 'method name shown at checkout, e.g. "Przelew tradycyjny"').option("--instructions <text>", "plain-text payment instructions for the buyer").option("--instructions-file <path>", "read the instructions from a file").action(
18484
+ async (options, command) => {
18485
+ const instructions = options.instructions ?? (options.instructionsFile ? readFileSync15(options.instructionsFile, "utf8").trim() : void 0);
18486
+ if (!instructions) throw new Error("--instructions or --instructions-file is required.");
18487
+ const flags = getGlobalFlags(command);
18488
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18489
+ const method = await client.configureManualPayment({ name: options.name, instructions });
18490
+ emit(flags, method, () => {
18491
+ process.stdout.write(`Manual payment: ${method.name}
18492
+ ${method.instructions}
18493
+ `);
18494
+ });
18495
+ }
18496
+ );
18497
+ manual.command("remove").description("Remove the manual payment method (idempotent). Requires --confirm.").option("--confirm", "required \u2014 buyers can no longer pick the method").action(async (options, command) => {
18498
+ requireConfirm7(options.confirm, "remove the manual payment method");
18499
+ const flags = getGlobalFlags(command);
18500
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18501
+ const result = await client.disconnectManualPayment();
18502
+ emit(flags, result, () => {
18503
+ process.stdout.write("Manual payment method removed\n");
18504
+ });
18505
+ });
18506
+ }
18507
+
18508
+ // src/commands/integrations/providers.ts
18509
+ import { readFileSync as readFileSync16 } from "fs";
18510
+ var INVOICES_ALIASES = {
18511
+ fakturownia: "fakturownia",
18512
+ ifirma: "ifirma",
18513
+ infakt: "inFakt",
18514
+ wfirma: "wFirmaV2",
18515
+ wfirmav2: "wFirmaV2",
18516
+ ing: "ingKsiegowosc",
18517
+ ingksiegowosc: "ingKsiegowosc",
18518
+ stripe: "stripe"
18519
+ };
18520
+ function parseInvoicesClient(value) {
18521
+ const match = INVOICES_ALIASES[value.trim().toLowerCase()];
18522
+ if (!match) {
18523
+ throw new Error(
18524
+ `Unknown invoicing client: ${value} (expected ${INVOICES_CLIENTS.join(" | ")})`
18525
+ );
18526
+ }
18527
+ return match;
18528
+ }
18529
+ function parseGateway(value) {
18530
+ const normalized = value.trim().toLowerCase().replace("p24", "przelewy24");
18531
+ const match = CREDENTIAL_PAYMENT_GATEWAYS.find((gateway) => gateway === normalized);
18532
+ if (!match) {
18533
+ throw new Error(
18534
+ `Unknown gateway: ${value} (expected ${CREDENTIAL_PAYMENT_GATEWAYS.join(" | ")}; Stripe connects in the dashboard, the transfer method is \`integrations manual-payment\`)`
18535
+ );
18536
+ }
18537
+ return match;
18538
+ }
18539
+ function parseEnum(value, allowed, flag) {
18540
+ const match = allowed.find((candidate) => candidate === value);
18541
+ if (!match) throw new Error(`Invalid ${flag}: ${value} (expected ${allowed.join(" | ")})`);
18542
+ return match;
18543
+ }
18544
+ function requireConfirm8(confirm, what) {
18545
+ if (!confirm) {
18546
+ throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
18547
+ }
18548
+ }
18549
+ function readSecret2(label, value, fromStdin, fromEnv) {
18550
+ const sources = [value !== void 0, fromStdin === true, fromEnv !== void 0].filter(Boolean);
18551
+ if (sources.length > 1) {
18552
+ throw new Error(`Pass ${label} through exactly one of the value, -stdin or -env forms.`);
18553
+ }
18554
+ if (fromStdin) {
18555
+ const raw = readFileSync16(0, "utf8").trim();
18556
+ if (!raw) throw new Error(`${label}: nothing was read from stdin.`);
18557
+ return raw;
18558
+ }
18559
+ if (fromEnv !== void 0) {
18560
+ const raw = process.env[fromEnv]?.trim();
18561
+ if (!raw) throw new Error(`${label}: environment variable ${fromEnv} is empty or unset.`);
18562
+ return raw;
18563
+ }
18564
+ return value;
18565
+ }
18566
+ function printPayments(payments) {
18567
+ const on = (value) => value ? "on" : "off";
18568
+ process.stdout.write(
18569
+ `Payments: stripe=${on(payments.stripe)} payu=${on(payments.payU)} tpay=${on(payments.tpay)} paynow=${on(payments.payNow)} przelewy24=${on(payments.przelewy24)} paypal=${on(payments.paypal)} dodo=${on(payments.dodo)} manual=${payments.manual ? "on" : "off"}
18570
+ `
18571
+ );
18572
+ }
18573
+ function buildInvoicesInput(client, options) {
18574
+ const secret = readSecret2("--api-key", options.apiKey, options.apiKeyStdin, options.apiKeyEnv);
18575
+ const input = { client };
18576
+ if (options.id) input.id = options.id;
18577
+ if (options.name) input.accountName = options.name;
18578
+ if (options.url) input.url = options.url;
18579
+ if (options.departmentId) input.departmentId = options.departmentId;
18580
+ switch (client) {
18581
+ case "wFirmaV2":
18582
+ if (!options.accessKey || !options.secretKey || !options.companyId) {
18583
+ throw new Error("wFirma needs --access-key, --secret-key and --company-id.");
18584
+ }
18585
+ input.accessKey = options.accessKey;
18586
+ input.secretKey = options.secretKey;
18587
+ input.companyId = options.companyId;
18588
+ break;
18589
+ case "stripe":
18590
+ break;
18591
+ case "ifirma":
18592
+ if (secret) input.apiKey = secret;
18593
+ if (options.accountApiKey) input.accountApiKey = options.accountApiKey;
18594
+ break;
18595
+ case "ingKsiegowosc":
18596
+ if (secret) input.apiKey = secret;
18597
+ if (options.bankAccount) input.bankAccountNumber = options.bankAccount;
18598
+ break;
18599
+ default:
18600
+ if (secret) input.apiKey = secret;
18601
+ }
18602
+ if (client !== "stripe" && !input.url) throw new Error("--url is required.");
18603
+ return input;
18604
+ }
18605
+ function resolveSecret(value, fromEnv, flag) {
18606
+ if (value === void 0 || !fromEnv) return value;
18607
+ const raw = process.env[value]?.trim();
18608
+ if (!raw) throw new Error(`${flag}: environment variable ${value} is empty or unset.`);
18609
+ return raw;
18610
+ }
18611
+ function buildGatewayInput(gateway, o) {
18612
+ const need = (value, flag) => {
18613
+ if (!value) throw new Error(`${flag} is required for ${gateway}.`);
18614
+ return value;
18615
+ };
18616
+ const secret = (value, flag) => need(resolveSecret(value, o.secretsEnv, flag), flag);
18617
+ const input = { gateway, name: o.name };
18618
+ switch (gateway) {
18619
+ case "payu":
18620
+ input.posId = need(o.posId, "--pos-id");
18621
+ input.clientId = need(o.clientId, "--client-id");
18622
+ input.clientSecret = secret(o.clientSecret, "--client-secret");
18623
+ input.clientSecondKey = secret(o.secondKey, "--second-key");
18624
+ break;
18625
+ case "tpay":
18626
+ input.posId = need(o.posId, "--pos-id");
18627
+ input.clientId = need(o.clientId, "--client-id");
18628
+ input.clientSecret = secret(o.clientSecret, "--client-secret");
18629
+ input.safetyKey = secret(o.safetyKey, "--safety-key");
18630
+ break;
18631
+ case "przelewy24":
18632
+ input.merchantId = need(o.merchantId, "--merchant-id");
18633
+ input.posId = need(o.posId, "--pos-id");
18634
+ input.apiKey = secret(o.apiKey, "--api-key");
18635
+ input.crcKey = secret(o.crcKey, "--crc-key");
18636
+ break;
18637
+ case "paynow":
18638
+ input.apiKey = secret(o.apiKey, "--api-key");
18639
+ input.signatureKey = secret(o.signatureKey, "--signature-key");
18640
+ break;
18641
+ case "paypal":
18642
+ input.clientId = need(o.clientId, "--client-id");
18643
+ input.clientSecret = secret(o.clientSecret, "--client-secret");
18644
+ break;
18645
+ case "dodo":
18646
+ input.apiKey = secret(o.apiKey, "--api-key");
18647
+ break;
18648
+ }
18649
+ return input;
18650
+ }
18651
+ var TWILIO_REGIONS = ["us1", "ie1", "au1"];
18652
+ var TWILIO_AUTH_KINDS = ["authToken", "apiKey"];
18653
+ var TWILIO_SENDER_KINDS = [
18654
+ "phoneNumber",
18655
+ "messagingService"
18656
+ ];
18657
+ var SMSAPI_REGIONS = ["pl", "com"];
18658
+ var SMS_PROVIDERS = ["twilio", "smsapi"];
18659
+ function registerIntegrationProviderCommands(integrations) {
18660
+ const invoices = integrations.command("invoices").description(
18661
+ "Invoicing accounts: fakturownia, ifirma, infakt, wfirma (v2), ing (ING Ksi\u0119gowo\u015B\u0107), stripe. Keys are verified with the provider and never printed."
18662
+ );
18663
+ invoices.command("connect <client>").description("Connect (or edit with --id) an invoicing account").option("--id <id>", "edit this existing account (from `integrations status`)").option("--name <name>", "creator-facing label (accountName)").option("--url <url>", "provider account URL / API base (not for stripe)").option("--api-key <key>", "API key (fakturownia / ifirma invoice key / infakt / ing)").option("--api-key-stdin", "read the API key from stdin").option("--api-key-env <VAR>", "read the API key from an environment variable").option("--account-api-key <key>", "ifirma: the account (user) API key").option("--access-key <key>", "wfirma").option("--secret-key <key>", "wfirma").option("--company-id <id>", "wfirma").option("--department-id <id>", "fakturownia / infakt / ifirma").option("--bank-account <iban>", "ing: bank account number for the invoice").action(async (clientArg, options, command) => {
18664
+ const flags = getGlobalFlags(command);
18665
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18666
+ const account = await client.connectInvoicesClient(
18667
+ buildInvoicesInput(parseInvoicesClient(clientArg), options)
18668
+ );
18669
+ emit(flags, account, () => {
18670
+ process.stdout.write(`Connected ${account.client} ${account.id} (${account.url})
18671
+ `);
18672
+ });
18673
+ });
18674
+ invoices.command("disconnect <client> <accountId>").description("Disconnect an invoicing account (irreversible). Requires --confirm.").option("--confirm", "required \u2014 acknowledges the stored key is deleted").action(
18675
+ async (clientArg, accountId, options, command) => {
18676
+ requireConfirm8(options.confirm, `disconnect ${clientArg} ${accountId}`);
18677
+ const flags = getGlobalFlags(command);
18678
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18679
+ const parsed = parseInvoicesClient(clientArg);
18680
+ const result = await client.disconnectInvoicesClient(parsed, accountId);
18681
+ emit(flags, { ...result, client: parsed, id: accountId }, () => {
18682
+ process.stdout.write(`Disconnected ${parsed} ${accountId}
18683
+ `);
18684
+ });
18685
+ }
18686
+ );
18687
+ const payments = integrations.command("payments").description(
18688
+ "Credential payment gateways: payu, tpay, przelewy24 (p24), paynow, paypal, dodo. Stripe connects in the dashboard (OAuth); the transfer method is `integrations manual-payment`."
18689
+ );
18690
+ payments.command("connect <gateway>").description(
18691
+ "Connect a gateway \u2014 credentials are verified with the gateway first. With --secrets-env every secret flag takes an environment variable NAME instead of the value."
18692
+ ).requiredOption("--name <name>", "creator-facing label").option("--pos-id <id>", "payu / tpay / przelewy24").option("--client-id <id>", "payu / tpay / paypal").option("--client-secret <secret>", "payu / tpay / paypal").option("--second-key <key>", "payu: the second (MD5) key").option("--safety-key <key>", "tpay: the security key").option("--api-key <key>", "przelewy24 / paynow / dodo").option("--signature-key <key>", "paynow").option("--merchant-id <id>", "przelewy24").option("--crc-key <key>", "przelewy24").option("--secrets-env", "treat every secret flag value as an environment variable name").action(async (gatewayArg, options, command) => {
18693
+ const flags = getGlobalFlags(command);
18694
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18695
+ const status = await client.connectPaymentGateway(
18696
+ buildGatewayInput(parseGateway(gatewayArg), options)
18697
+ );
18698
+ emit(flags, status, () => printPayments(status));
18699
+ });
18700
+ payments.command("disconnect <gateway>").description("Disconnect a gateway (irreversible, secrets deleted). Requires --confirm.").option("--confirm", "required \u2014 prices that only offered this gateway become unbuyable").action(async (gatewayArg, options, command) => {
18701
+ requireConfirm8(options.confirm, `disconnect ${gatewayArg}`);
18702
+ const flags = getGlobalFlags(command);
18703
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18704
+ const status = await client.disconnectPaymentGateway(parseGateway(gatewayArg));
18705
+ emit(flags, status, () => printPayments(status));
18706
+ });
18707
+ payments.command("tpay-blik <on|off>").description("Turn Tpay recurring BLIK (BLIK subscriptions) on or off").action(async (value, _options, command) => {
18708
+ const enabled = parseEnum(value, ["on", "off"], "tpay-blik") === "on";
18709
+ const flags = getGlobalFlags(command);
18710
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18711
+ const result = await client.setTpayRecurringBlik(enabled);
18712
+ emit(flags, { ...result, enabled }, () => {
18713
+ process.stdout.write(`Tpay recurring BLIK: ${enabled ? "on" : "off"}
18714
+ `);
18715
+ });
18716
+ });
18717
+ const sms = integrations.command("sms").description("SMS providers (Twilio, SMSAPI) and which one campaigns send through");
18718
+ const twilio = sms.command("twilio").description("Twilio account");
18719
+ twilio.command("connect").description("Connect (or replace) the Twilio account \u2014 credentials verified with Twilio first").requiredOption("--account-sid <sid>", "Twilio account SID").option("--auth-token <token>", "auth token, or the API key secret with --auth-kind apiKey").option("--auth-token-stdin", "read the token from stdin").option("--auth-token-env <VAR>", "read the token from an environment variable").option("--auth-kind <authToken|apiKey>", "default authToken").option("--api-key-sid <sid>", "with --auth-kind apiKey: the API key SID (SK\u2026)").option("--region <us1|ie1|au1>", "default us1").requiredOption("--sender-kind <phoneNumber|messagingService>", "what --sender is").requiredOption("--sender <value>", "phone number (E.164) or messaging service SID (MG\u2026)").action(
18720
+ async (options, command) => {
18721
+ const token = readSecret2(
18722
+ "--auth-token",
18723
+ options.authToken,
18724
+ options.authTokenStdin,
18725
+ options.authTokenEnv
18726
+ );
18727
+ if (!token)
18728
+ throw new Error(
18729
+ "--auth-token (or --auth-token-stdin / --auth-token-env VAR) is required."
18730
+ );
18731
+ const input = {
18732
+ accountSid: options.accountSid,
18733
+ authToken: token,
18734
+ senderKind: parseEnum(options.senderKind, TWILIO_SENDER_KINDS, "--sender-kind"),
18735
+ sender: options.sender
18736
+ };
18737
+ if (options.region) input.region = parseEnum(options.region, TWILIO_REGIONS, "--region");
18738
+ if (options.authKind)
18739
+ input.authKind = parseEnum(options.authKind, TWILIO_AUTH_KINDS, "--auth-kind");
18740
+ if (options.apiKeySid) input.apiKeySid = options.apiKeySid;
18741
+ const flags = getGlobalFlags(command);
18742
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18743
+ const result = await client.connectTwilio(input);
18744
+ emit(flags, result, () => {
18745
+ process.stdout.write(
18746
+ `Connected Twilio ${result.accountSid} (${result.region}) sender ${result.sender}
18747
+ `
18748
+ );
18749
+ });
18750
+ }
18751
+ );
18752
+ twilio.command("disconnect").description("Disconnect Twilio (secret deleted). Requires --confirm.").option("--confirm", "required").action(async (options, command) => {
18753
+ requireConfirm8(options.confirm, "disconnect Twilio");
18754
+ const flags = getGlobalFlags(command);
18755
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18756
+ const result = await client.disconnectTwilio();
18757
+ emit(flags, result, () => {
18758
+ process.stdout.write("Twilio disconnected\n");
18759
+ });
18760
+ });
18761
+ const smsapi = sms.command("smsapi").description("SMSAPI account (smsapi.pl / smsapi.com)");
18762
+ smsapi.command("connect").description(
18763
+ "Connect (or replace) the SMSAPI account \u2014 the token is verified with SMSAPI first"
18764
+ ).option("--region <pl|com>", "default pl").option("--api-token <token>", "OAuth API token (prefer -stdin / -env)").option("--api-token-stdin", "read the token from stdin").option("--api-token-env <VAR>", "read the token from an environment variable").requiredOption("--sender <name>", "approved sender name (max 11 chars)").action(
18765
+ async (options, command) => {
18766
+ const token = readSecret2(
18767
+ "--api-token",
18768
+ options.apiToken,
18769
+ options.apiTokenStdin,
18770
+ options.apiTokenEnv
18771
+ );
18772
+ if (!token)
18773
+ throw new Error("--api-token (or --api-token-stdin / --api-token-env VAR) is required.");
18774
+ const flags = getGlobalFlags(command);
18775
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18776
+ const result = await client.connectSmsApi({
18777
+ region: options.region ? parseEnum(options.region, SMSAPI_REGIONS, "--region") : "pl",
18778
+ apiToken: token,
18779
+ sender: options.sender
18780
+ });
18781
+ emit(flags, result, () => {
18782
+ process.stdout.write(
18783
+ `Connected SMSAPI (${result.region}) sender ${result.sender}${result.points !== null ? `, ${result.points} points` : ""}
18784
+ `
18785
+ );
18786
+ });
18787
+ }
18788
+ );
18789
+ smsapi.command("disconnect").description("Disconnect SMSAPI (token deleted). Requires --confirm.").option("--confirm", "required").action(async (options, command) => {
18790
+ requireConfirm8(options.confirm, "disconnect SMSAPI");
18791
+ const flags = getGlobalFlags(command);
18792
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18793
+ const result = await client.disconnectSmsApi();
18794
+ emit(flags, result, () => {
18795
+ process.stdout.write("SMSAPI disconnected\n");
18796
+ });
18797
+ });
18798
+ smsapi.command("refresh-senders").description("Re-fetch the approved SMSAPI sender names").action(async (_options, command) => {
18799
+ const flags = getGlobalFlags(command);
18800
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18801
+ const result = await client.refreshSmsApiSenders();
18802
+ emit(flags, result, () => {
18803
+ process.stdout.write(
18804
+ `Senders: ${result.senders.join(", ") || "(none)"}
18805
+ Default: ${result.sender}
18806
+ `
18807
+ );
18808
+ });
18809
+ });
18810
+ smsapi.command("default-sender <sender>").description("Pick which approved SMSAPI sender campaigns send from").action(async (sender, _options, command) => {
18811
+ const flags = getGlobalFlags(command);
18812
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18813
+ const result = await client.setDefaultSmsApiSender(sender);
18814
+ emit(flags, result, () => {
18815
+ process.stdout.write(`Default SMSAPI sender: ${result.sender}
18816
+ `);
18817
+ });
18818
+ });
18819
+ sms.command("default-provider <twilio|smsapi>").description("Choose which connected SMS provider campaigns and automations send through").action(async (value, _options, command) => {
18820
+ const provider = parseEnum(value, SMS_PROVIDERS, "default-provider");
18821
+ const flags = getGlobalFlags(command);
18822
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18823
+ const result = await client.setDefaultSmsProvider(provider);
18824
+ emit(flags, result, () => {
18825
+ process.stdout.write(`Default SMS provider: ${result.defaultProvider}
18826
+ `);
18827
+ });
18828
+ });
18829
+ }
18830
+
18831
+ // src/commands/integrations/index.ts
18832
+ var CLIENT_ALIASES = {
18833
+ gtm: "googleTagManager",
18834
+ ga4: "googleAnalytics",
18835
+ ga: "googleAnalytics",
18836
+ clarity: "microsoftClarity",
18837
+ twitter: "x"
18838
+ };
18839
+ var NONE = "none";
18840
+ function parseClient(value) {
18841
+ const alias = CLIENT_ALIASES[value.trim().toLowerCase()];
18842
+ if (alias) return alias;
18843
+ const match = MANAGED_CONVERSION_TRACKING_CLIENTS.find(
18844
+ (client) => client.toLowerCase() === value.trim().toLowerCase()
18845
+ );
18846
+ if (!match) {
18847
+ throw new Error(
18848
+ `Unknown platform: ${value} (expected ${MANAGED_CONVERSION_TRACKING_CLIENTS.join(" | ")}; aliases: ${Object.keys(CLIENT_ALIASES).join(", ")}). Meta pixel / CAPI is dashboard-only.`
18849
+ );
18850
+ }
18851
+ return match;
18852
+ }
18853
+ function parseOnOff3(value, flag) {
18854
+ if (value === void 0) return void 0;
18855
+ if (value === "on") return true;
18856
+ if (value === "off") return false;
18857
+ throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
18858
+ }
18859
+ function textOrNull(value) {
18860
+ if (value === void 0) return void 0;
18861
+ return value.trim().toLowerCase() === NONE ? null : value;
18862
+ }
18863
+ function requireConfirm9(confirm, what) {
18864
+ if (!confirm) {
18865
+ throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
18866
+ }
18867
+ }
18868
+ var TRACK_FLAG_DEFAULTS = {
18869
+ trackPurchase: true,
18870
+ trackInitCheckout: true,
18871
+ trackAddToCart: true,
18872
+ trackTrialStarted: false
18873
+ };
18874
+ var REQUIRED_ID_FLAG = {
18875
+ googleAnalytics: "measurementId",
18876
+ googleTagManager: "containerId",
18877
+ tiktok: "pixelId",
18878
+ x: "pixelId",
18879
+ linkedin: "partnerId",
18880
+ microsoftClarity: "projectId",
18881
+ hotjar: "siteId"
18882
+ };
18883
+ var FLAG_NAME = {
18884
+ name: "--name",
18885
+ containerId: "--container-id",
18886
+ measurementId: "--measurement-id",
18887
+ apiSecret: "--api-secret",
18888
+ pixelId: "--pixel-id",
18889
+ accessToken: "--access-token",
18890
+ partnerId: "--partner-id",
18891
+ projectId: "--project-id",
18892
+ siteId: "--site-id",
18893
+ purchaseTag: "--purchase-tag",
18894
+ initCheckoutTag: "--init-checkout-tag",
18895
+ addToCartTag: "--add-to-cart-tag",
18896
+ trialStartedTag: "--trial-started-tag",
18897
+ trackPurchase: "--track-purchase",
18898
+ trackInitCheckout: "--track-init-checkout",
18899
+ trackAddToCart: "--track-add-to-cart",
18900
+ trackTrialStarted: "--track-trial-started"
18901
+ };
18902
+ var hasEventFlags = (client) => client === "googleAnalytics" || client === "tiktok" || client === "x" || client === "linkedin";
18903
+ function buildUpsertInput(client, options, existing) {
18904
+ const name = options.name ?? existing?.name;
18905
+ if (!name) throw new Error("--name is required when adding an integration.");
18906
+ const input = { client, name };
18907
+ if (existing) input.integrationId = existing.id;
18908
+ const existingFields = existing ? { ...existing } : {};
18909
+ const pick = (key, provided) => {
18910
+ if (provided !== void 0) return provided;
18911
+ const value = existingFields[key];
18912
+ return typeof value === "string" ? value : void 0;
18913
+ };
18914
+ switch (client) {
18915
+ case "googleTagManager":
18916
+ input.containerId = pick("containerId", options.containerId);
18917
+ break;
18918
+ case "googleAnalytics":
18919
+ input.measurementId = pick("measurementId", options.measurementId);
18920
+ if (options.apiSecret !== void 0) input.apiSecret = textOrNull(options.apiSecret);
18921
+ break;
18922
+ case "tiktok":
18923
+ input.pixelId = pick("pixelId", options.pixelId);
18924
+ if (options.accessToken !== void 0) input.accessToken = textOrNull(options.accessToken);
18925
+ break;
18926
+ case "x":
18927
+ input.pixelId = pick("pixelId", options.pixelId);
18928
+ if (options.accessToken !== void 0) input.accessToken = textOrNull(options.accessToken);
18929
+ input.purchaseEventTag = textOrNull(options.purchaseTag) ?? pick("purchaseEventTag", void 0) ?? null;
18930
+ input.initCheckoutEventTag = textOrNull(options.initCheckoutTag) ?? pick("initCheckoutEventTag", void 0) ?? null;
18931
+ input.addToCartEventTag = textOrNull(options.addToCartTag) ?? pick("addToCartEventTag", void 0) ?? null;
18932
+ input.trialStartedEventTag = textOrNull(options.trialStartedTag) ?? pick("trialStartedEventTag", void 0) ?? null;
18933
+ break;
18934
+ case "linkedin":
18935
+ input.partnerId = pick("partnerId", options.partnerId);
18936
+ if (options.accessToken !== void 0) input.accessToken = textOrNull(options.accessToken);
18937
+ input.purchaseConversionUrn = textOrNull(options.purchaseTag) ?? pick("purchaseConversionUrn", void 0) ?? null;
18938
+ input.initCheckoutConversionUrn = textOrNull(options.initCheckoutTag) ?? pick("initCheckoutConversionUrn", void 0) ?? null;
18939
+ input.addToCartConversionUrn = textOrNull(options.addToCartTag) ?? pick("addToCartConversionUrn", void 0) ?? null;
18940
+ input.trialStartedConversionUrn = textOrNull(options.trialStartedTag) ?? pick("trialStartedConversionUrn", void 0) ?? null;
18941
+ break;
18942
+ case "microsoftClarity":
18943
+ input.projectId = pick("projectId", options.projectId);
18944
+ break;
18945
+ case "hotjar":
18946
+ input.siteId = pick("siteId", options.siteId);
18947
+ break;
18948
+ }
18949
+ const requiredKey = REQUIRED_ID_FLAG[client];
18950
+ if (input[requiredKey] === void 0) {
18951
+ throw new Error(`${FLAG_NAME[requiredKey]} is required for ${client}.`);
18952
+ }
18953
+ if (hasEventFlags(client)) {
18954
+ const flags = existing && "trackPurchase" in existing ? existing : TRACK_FLAG_DEFAULTS;
18955
+ input.trackPurchase = parseOnOff3(options.trackPurchase, FLAG_NAME.trackPurchase) ?? flags.trackPurchase;
18956
+ input.trackInitCheckout = parseOnOff3(options.trackInitCheckout, FLAG_NAME.trackInitCheckout) ?? flags.trackInitCheckout;
18957
+ input.trackAddToCart = parseOnOff3(options.trackAddToCart, FLAG_NAME.trackAddToCart) ?? flags.trackAddToCart;
18958
+ input.trackTrialStarted = parseOnOff3(options.trackTrialStarted, FLAG_NAME.trackTrialStarted) ?? flags.trackTrialStarted;
18959
+ }
18960
+ return input;
18961
+ }
18962
+ function platformIdOf(integration) {
18963
+ switch (integration.client) {
18964
+ case "googleAnalytics":
18965
+ return integration.measurementId;
18966
+ case "googleTagManager":
18967
+ return integration.containerId;
18968
+ case "tiktok":
18969
+ case "x":
18970
+ return integration.pixelId;
18971
+ case "linkedin":
18972
+ return integration.partnerId;
18973
+ case "microsoftClarity":
18974
+ return integration.projectId;
18975
+ case "hotjar":
18976
+ return integration.siteId;
18977
+ }
18978
+ }
18979
+ function serverSideOf(integration) {
18980
+ if ("apiSecretPath" in integration) return integration.apiSecretPath ? "yes" : "";
18981
+ return "n/a";
18982
+ }
18983
+ function printIntegrations(integrations) {
18984
+ printTable(
18985
+ integrations.map((integration) => ({
18986
+ id: integration.id,
18987
+ platform: integration.client,
18988
+ name: integration.name,
18989
+ platformId: platformIdOf(integration),
18990
+ serverSide: serverSideOf(integration),
18991
+ default: integration.isWorkspaceDefault ? "yes" : "",
18992
+ updatedAt: integration.updatedAt
18993
+ })),
18994
+ ["id", "platform", "name", "platformId", "serverSide", "default", "updatedAt"]
18995
+ );
18996
+ }
18997
+ function printIntegration(integration, headline) {
18998
+ process.stdout.write(`${headline} ${integration.id} (${integration.client})
18999
+ `);
19000
+ process.stdout.write(` Name: ${integration.name}
19001
+ `);
19002
+ process.stdout.write(` Platform id: ${platformIdOf(integration)}
19003
+ `);
19004
+ process.stdout.write(` Workspace default: ${integration.isWorkspaceDefault ? "yes" : "no"}
19005
+ `);
19006
+ if ("trackPurchase" in integration) {
19007
+ process.stdout.write(
19008
+ ` Events: purchase=${integration.trackPurchase ? "on" : "off"} initCheckout=${integration.trackInitCheckout ? "on" : "off"} addToCart=${integration.trackAddToCart ? "on" : "off"} trialStarted=${integration.trackTrialStarted ? "on" : "off"}
19009
+ `
19010
+ );
19011
+ }
19012
+ if ("apiSecretPath" in integration) {
19013
+ process.stdout.write(` Server-side events: ${integration.apiSecretPath ? "on" : "off"}
19014
+ `);
19015
+ }
19016
+ }
19017
+ function addPlatformOptions(command) {
19018
+ return command.option("--name <name>", "creator-facing label").option("--container-id <id>", "googleTagManager: GTM container id, e.g. GTM-ABCD123").option("--measurement-id <id>", "googleAnalytics: GA4 measurement id, e.g. G-XXXXXXXXXX").option(
19019
+ "--api-secret <secret|none>",
19020
+ "googleAnalytics: Measurement Protocol API secret for server-side events (validated against GA4); none = client-side only"
19021
+ ).option("--pixel-id <id>", "tiktok / x: pixel id").option(
19022
+ "--access-token <token|none>",
19023
+ "tiktok / x / linkedin: events API access token for server-side events (TikTok/LinkedIn tokens are validated); none = client-side only"
19024
+ ).option("--partner-id <id>", "linkedin: Insight Tag partner id (6-12 digits)").option("--project-id <id>", "microsoftClarity: project id").option("--site-id <id>", "hotjar: site id").option(
19025
+ "--purchase-tag <tag|none>",
19026
+ "x: purchase event tag / linkedin: purchase conversion URN"
19027
+ ).option("--init-checkout-tag <tag|none>", "x / linkedin: initiate-checkout tag / URN").option("--add-to-cart-tag <tag|none>", "x / linkedin: add-to-cart tag / URN").option("--trial-started-tag <tag|none>", "x / linkedin: trial-started tag / URN").option("--track-purchase <on|off>", "GA4 / TikTok / X / LinkedIn: send Purchase (default on)").option(
19028
+ "--track-init-checkout <on|off>",
19029
+ "GA4 / TikTok / X / LinkedIn: send InitiateCheckout (default on)"
19030
+ ).option(
19031
+ "--track-add-to-cart <on|off>",
19032
+ "GA4 / TikTok / X / LinkedIn: send AddToCart (default on)"
19033
+ ).option(
19034
+ "--track-trial-started <on|off>",
19035
+ "GA4 / TikTok / X / LinkedIn: send TrialStarted (default off)"
19036
+ );
19037
+ }
19038
+ function registerIntegrationsCommands(program2) {
19039
+ const integrations = program2.command("integrations").description(
19040
+ "Conversion-tracking integrations (Settings \u2192 Integrations \u2192 Analytics): Google Tag Manager, Google Analytics 4, TikTok, X, LinkedIn, Microsoft Clarity, Hotjar. Meta pixel / Conversions API, payment gateways and email clients stay dashboard-only for now. Assign per offer with `offers update --tracking-integration-ids`, or workspace-wide with `integrations set-defaults`."
19041
+ );
19042
+ registerIntegrationConnectionCommands(integrations);
19043
+ registerIntegrationProviderCommands(integrations);
19044
+ integrations.command("list").description("List the managed conversion-tracking integrations and the workspace defaults").option(
19045
+ "--client <platform>",
19046
+ "only one platform (e.g. gtm, ga4, tiktok, x, linkedin, clarity, hotjar)"
19047
+ ).action(async (options, command) => {
19048
+ const flags = getGlobalFlags(command);
19049
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19050
+ const result = await client.listConversionTrackingIntegrations();
19051
+ const filter = options.client !== void 0 ? parseClient(options.client) : void 0;
19052
+ const entries = filter ? result.integrations.filter((integration) => integration.client === filter) : result.integrations;
19053
+ emit(flags, { ...result, integrations: entries }, () => {
19054
+ printIntegrations(entries);
19055
+ process.stdout.write(
19056
+ `Workspace defaults: ${result.defaultIntegrationIds.length > 0 ? result.defaultIntegrationIds.join(", ") : "(none)"}
19057
+ `
19058
+ );
19059
+ });
19060
+ });
19061
+ addPlatformOptions(
19062
+ integrations.command("add <platform>").description(
19063
+ "Add a conversion-tracking integration. The first one a workspace connects becomes the workspace default automatically; later ones need `integrations set-defaults` or a per-offer assignment. Platform: gtm | ga4 | tiktok | x | linkedin | clarity | hotjar"
19064
+ )
19065
+ ).action(async (platform2, options, command) => {
19066
+ const flags = getGlobalFlags(command);
19067
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19068
+ const input = buildUpsertInput(parseClient(platform2), options, null);
19069
+ const result = await client.upsertConversionTrackingIntegration(input);
19070
+ emit(flags, result, () => printIntegration(result, "Added"));
19071
+ });
19072
+ addPlatformOptions(
19073
+ integrations.command("update <platform> <integrationId>").description(
19074
+ "Edit an integration \u2014 only the flags you pass change; omitted secrets stay stored. Ids come from `integrations list`."
19075
+ )
19076
+ ).action(
19077
+ async (platform2, integrationId, options, command) => {
19078
+ const flags = getGlobalFlags(command);
19079
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19080
+ const parsedClient = parseClient(platform2);
19081
+ const current = await client.listConversionTrackingIntegrations();
19082
+ const existing = current.integrations.find(
19083
+ (integration) => integration.id === integrationId && integration.client === parsedClient
19084
+ );
19085
+ if (!existing) {
19086
+ throw new Error(
19087
+ `No ${parsedClient} integration with id ${integrationId}. Run \`zanfia integrations list\`.`
19088
+ );
19089
+ }
19090
+ const input = buildUpsertInput(parsedClient, options, existing);
19091
+ const result = await client.upsertConversionTrackingIntegration(input);
19092
+ emit(flags, result, () => printIntegration(result, "Updated"));
19093
+ }
19094
+ );
19095
+ integrations.command("remove <platform> <integrationId>").description(
19096
+ "Remove an integration (irreversible). Offers and the workspace defaults that still list the id simply stop tracking on that platform. Requires --confirm."
19097
+ ).option("--confirm", "required \u2014 acknowledges the integration is deleted").action(
19098
+ async (platform2, integrationId, options, command) => {
19099
+ requireConfirm9(options.confirm, `remove integration ${integrationId}`);
19100
+ const flags = getGlobalFlags(command);
19101
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19102
+ const result = await client.removeConversionTrackingIntegration(
19103
+ parseClient(platform2),
19104
+ integrationId
19105
+ );
19106
+ emit(flags, { ...result, integrationId }, () => {
19107
+ process.stdout.write(`Removed integration ${integrationId}
19108
+ `);
19109
+ });
19110
+ }
19111
+ );
19112
+ integrations.command("set-defaults <ids|none>").description(
19113
+ "Replace the workspace-default integrations (comma-separated ids; none = clear) applied to every offer that inherits defaults. Meta ids from `stats conversion-health` may be included."
19114
+ ).action(async (ids, _options, command) => {
19115
+ const flags = getGlobalFlags(command);
19116
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19117
+ const integrationIds = ids.trim().toLowerCase() === NONE ? [] : ids.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
19118
+ const result = await client.setDefaultConversionTracking(integrationIds);
19119
+ emit(flags, result, () => {
19120
+ process.stdout.write(
19121
+ `Workspace defaults: ${result.integrationIds.length > 0 ? result.integrationIds.join(", ") : "(none)"}
19122
+ `
19123
+ );
19124
+ });
19125
+ });
19126
+ }
19127
+
19128
+ // src/commands/join/index.ts
19129
+ var MAX_CODE_ATTEMPTS2 = 5;
19130
+ var HANDLE_REGEX = /^[a-z0-9][a-z0-9-]{1,62}$/;
19131
+ var EMAIL_REGEX2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
19132
+ var AGENT_DOCS_URL = "https://zanfia.com/help/developer-cli/agents";
19133
+ function joinUrl(base, handle, step) {
19134
+ return `${base.replace(/\/$/, "")}/v1/join/${encodeURIComponent(handle)}/${step}`;
19135
+ }
19136
+ async function runJoin(handleInput, options) {
19137
+ const handle = handleInput.trim().toLowerCase();
19138
+ if (!HANDLE_REGEX.test(handle)) {
19139
+ throw new Error(
19140
+ "Community handle must be lowercase letters, digits and dashes (e.g. crew-of-one)."
19141
+ );
19142
+ }
19143
+ const communityApiUrl = resolveCommunityApiUrl();
19144
+ if (!communityApiUrl) {
19145
+ throw new Error(
19146
+ "No Community API URL configured. Set ZANFIA_COMMUNITY_API_URL (this build has no baked backend)."
19147
+ );
19148
+ }
19149
+ const verbose = options.verbose === true;
19150
+ const email = (options.email ?? await promptLine("Email: ")).trim().toLowerCase();
19151
+ if (!EMAIL_REGEX2.test(email)) {
19152
+ throw new Error("Please enter a valid email address.");
19153
+ }
19154
+ const started = await postJson2(
19155
+ "Join",
19156
+ joinUrl(communityApiUrl, handle, "start"),
19157
+ { email, lang: "en" },
19158
+ { verbose }
19159
+ );
19160
+ if (!options.json) {
19161
+ process.stdout.write(
19162
+ `Joining ${started.communityName} (${started.workspaceName}) \u2014 a 6-digit code was sent to ${email}.
19163
+ `
17690
19164
  );
17691
19165
  }
17692
19166
  const firstName = options.name ?? (options.json ? "" : await promptLine("Your name (optional): "));
@@ -17790,8 +19264,80 @@ join again to refresh it.
17790
19264
  });
17791
19265
  }
17792
19266
 
19267
+ // src/commands/liveRooms/index.ts
19268
+ var minutes = (seconds) => `${Math.round(seconds / 60)} min`;
19269
+ var percent = (part, whole) => whole > 0 ? `${Math.round(part / whole * 100)}%` : "-";
19270
+ function describeSchedule(room) {
19271
+ if (!room.schedule) return "fixed";
19272
+ const until = room.schedule.until ? ` until ${room.schedule.until}` : "";
19273
+ return `every ${room.schedule.everyMinutes} min${until}`;
19274
+ }
19275
+ function registerLiveRoomsCommands(program2) {
19276
+ const liveRooms = program2.command("live-rooms").description(
19277
+ "Live rooms (webinar rooms, incl. rolling autowebinar sessions) \u2014 list and attendance; rooms are managed in the dashboard"
19278
+ );
19279
+ liveRooms.command("list").description("List live rooms (latest start first)").action(async (_, command) => {
19280
+ const flags = getGlobalFlags(command);
19281
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19282
+ const result = await client.listLiveRooms();
19283
+ emit(flags, result, () => {
19284
+ printTable(
19285
+ result.rooms.map((room) => ({
19286
+ id: room.id,
19287
+ title: room.title,
19288
+ source: room.source,
19289
+ status: room.endedAt ? "ended" : room.status,
19290
+ startAt: room.startAt,
19291
+ schedule: describeSchedule(room),
19292
+ access: room.access.everyone ? "everyone" : "restricted"
19293
+ })),
19294
+ ["id", "title", "source", "status", "startAt", "schedule", "access"]
19295
+ );
19296
+ });
19297
+ });
19298
+ liveRooms.command("attendance").description("A room's attendance per session; --session adds that session's viewers").argument("<roomId>", "live room id (from live-rooms list)").option("--session <iso>", "ISO start of one session to list its attendees").action(async (roomId, options, command) => {
19299
+ const flags = getGlobalFlags(command);
19300
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19301
+ const result = await client.getLiveRoomAttendance(roomId, options.session);
19302
+ emit(flags, result, () => {
19303
+ printTable(
19304
+ result.sessions.map((session) => ({
19305
+ session: session.sessionStartAt,
19306
+ attendees: session.attendees,
19307
+ identified: session.identified,
19308
+ avgWatched: `${minutes(session.averageWatchedSeconds)} (${percent(
19309
+ session.averageWatchedSeconds,
19310
+ session.durationSeconds
19311
+ )})`,
19312
+ toEnd: `${session.watchedToEnd} (${percent(session.watchedToEnd, session.attendees)})`,
19313
+ sawOffer: session.sawOffer,
19314
+ clickedOffer: session.clickedOffer
19315
+ })),
19316
+ ["session", "attendees", "identified", "avgWatched", "toEnd", "sawOffer", "clickedOffer"]
19317
+ );
19318
+ if (options.session !== void 0) {
19319
+ process.stdout.write(`
19320
+ Viewers of ${options.session}:
19321
+ `);
19322
+ printTable(
19323
+ result.attendees.map((attendee) => ({
19324
+ who: attendee.displayName ?? attendee.email ?? attendee.attendee,
19325
+ email: attendee.email ?? "",
19326
+ clientId: attendee.clientId ?? "",
19327
+ watched: minutes(attendee.watchedSeconds),
19328
+ reached: minutes(attendee.maxPositionSeconds),
19329
+ firstSeenAt: attendee.firstSeenAt,
19330
+ offer: attendee.purchasedAt ? "bought" : attendee.offerClickedAt ? "clicked" : attendee.offerSeenAt ? "saw" : ""
19331
+ })),
19332
+ ["who", "email", "clientId", "watched", "reached", "firstSeenAt", "offer"]
19333
+ );
19334
+ }
19335
+ });
19336
+ });
19337
+ }
19338
+
17793
19339
  // src/commands/media/index.ts
17794
- import { existsSync, readFileSync as readFileSync15, writeFileSync as writeFileSync4 } from "fs";
19340
+ import { existsSync, readFileSync as readFileSync17, writeFileSync as writeFileSync4 } from "fs";
17795
19341
  import { readFile, stat as stat3 } from "fs/promises";
17796
19342
  import { basename } from "path";
17797
19343
 
@@ -20909,16 +22455,61 @@ async function uploadVideoToBunny(params) {
20909
22455
  });
20910
22456
  }
20911
22457
 
22458
+ // src/utils/mime.ts
22459
+ var OCTET_STREAM_MIME = "application/octet-stream";
22460
+ function mimeFromName(name) {
22461
+ const n = name.toLowerCase();
22462
+ if (n.endsWith(".png")) return "image/png";
22463
+ if (n.endsWith(".jpg") || n.endsWith(".jpeg")) return "image/jpeg";
22464
+ if (n.endsWith(".webp")) return "image/webp";
22465
+ if (n.endsWith(".gif")) return "image/gif";
22466
+ if (n.endsWith(".svg")) return "image/svg+xml";
22467
+ if (n.endsWith(".avif")) return "image/avif";
22468
+ if (n.endsWith(".pdf")) return "application/pdf";
22469
+ if (n.endsWith(".zip")) return "application/zip";
22470
+ if (n.endsWith(".mp3")) return "audio/mpeg";
22471
+ if (n.endsWith(".mp4")) return "video/mp4";
22472
+ if (n.endsWith(".txt") || n.endsWith(".md")) return "text/plain";
22473
+ if (n.endsWith(".csv")) return "text/csv";
22474
+ if (n.endsWith(".json")) return "application/json";
22475
+ if (n.endsWith(".epub")) return "application/epub+zip";
22476
+ if (n.endsWith(".docx")) {
22477
+ return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
22478
+ }
22479
+ if (n.endsWith(".xlsx")) {
22480
+ return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
22481
+ }
22482
+ return null;
22483
+ }
22484
+ function mimeFromBytes(bytes) {
22485
+ const ascii = (start, text) => bytes.length >= start + text.length && bytes.toString("latin1", start, start + text.length) === text;
22486
+ if (bytes.length >= 8 && bytes[0] === 137 && ascii(1, "PNG\r\n\n")) return "image/png";
22487
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
22488
+ return "image/jpeg";
22489
+ }
22490
+ if (ascii(0, "GIF87a") || ascii(0, "GIF89a")) return "image/gif";
22491
+ if (ascii(0, "RIFF") && ascii(8, "WEBP")) return "image/webp";
22492
+ if (ascii(4, "ftypavif")) return "image/avif";
22493
+ if (ascii(0, "%PDF-")) return "application/pdf";
22494
+ if (bytes.toString("utf8", 0, Math.min(bytes.length, 1024)).toLowerCase().includes("<svg")) {
22495
+ return "image/svg+xml";
22496
+ }
22497
+ return null;
22498
+ }
22499
+ function inferMimeType(name, bytes) {
22500
+ return mimeFromName(name) ?? mimeFromBytes(bytes) ?? OCTET_STREAM_MIME;
22501
+ }
22502
+
20912
22503
  // src/commands/media/index.ts
20913
22504
  var DEFAULT_AI_IMAGE_ASPECT = "1:1" /* Square */;
20914
22505
  var DEFAULT_AI_IMAGE_QUALITY = "1K" /* Standard */;
20915
- function requireConfirm7(confirm, what) {
22506
+ function requireConfirm10(confirm, what) {
20916
22507
  if (!confirm) {
20917
22508
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
20918
22509
  }
20919
22510
  }
20920
22511
  function resolveTextOrFile(value) {
20921
- const text = existsSync(value) ? readFileSync15(value, "utf8") : value;
22512
+ const text = existsSync(value) ? readFileSync17(value, "utf8") : value;
20922
22513
  if (!text.trim()) throw new Error("The text (or the file it points to) is empty.");
20923
22514
  return text.trim();
20924
22515
  }
@@ -20974,32 +22565,6 @@ var TRANSCRIBE_STATUS_TEXT = {
20974
22565
  ["already_running" /* AlreadyRunning */]: "Transcription is already running",
20975
22566
  ["already_transcribed" /* AlreadyTranscribed */]: "Already transcribed \u2014 nothing queued (pass --force to re-transcribe)"
20976
22567
  };
20977
- function mimeFromName(name) {
20978
- const n = name.toLowerCase();
20979
- if (n.endsWith(".png")) return "image/png";
20980
- if (n.endsWith(".jpg") || n.endsWith(".jpeg")) return "image/jpeg";
20981
- if (n.endsWith(".webp")) return "image/webp";
20982
- if (n.endsWith(".gif")) return "image/gif";
20983
- if (n.endsWith(".svg")) return "image/svg+xml";
20984
- if (n.endsWith(".avif")) return "image/avif";
20985
- if (n.endsWith(".pdf")) return "application/pdf";
20986
- return null;
20987
- }
20988
- function mimeFromBytes(bytes) {
20989
- const ascii = (start, text) => bytes.length >= start + text.length && bytes.toString("latin1", start, start + text.length) === text;
20990
- if (bytes.length >= 8 && bytes[0] === 137 && ascii(1, "PNG\r\n\n")) return "image/png";
20991
- if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
20992
- return "image/jpeg";
20993
- }
20994
- if (ascii(0, "GIF87a") || ascii(0, "GIF89a")) return "image/gif";
20995
- if (ascii(0, "RIFF") && ascii(8, "WEBP")) return "image/webp";
20996
- if (ascii(4, "ftypavif")) return "image/avif";
20997
- if (ascii(0, "%PDF-")) return "application/pdf";
20998
- if (bytes.toString("utf8", 0, Math.min(bytes.length, 1024)).toLowerCase().includes("<svg")) {
20999
- return "image/svg+xml";
21000
- }
21001
- return null;
21002
- }
21003
22568
  function videoMimeFromName(name) {
21004
22569
  const n = name.toLowerCase();
21005
22570
  if (n.endsWith(".mov")) return "video/quicktime";
@@ -21022,7 +22587,7 @@ function registerMediaCommands(program2) {
21022
22587
  const name = options.name ?? basename(options.file);
21023
22588
  const input = {
21024
22589
  name,
21025
- mimeType: options.mime ?? mimeFromName(name) ?? mimeFromBytes(bytes) ?? "application/octet-stream",
22590
+ mimeType: options.mime ?? inferMimeType(name, bytes),
21026
22591
  data: bytes.toString("base64"),
21027
22592
  folder: options.folder,
21028
22593
  description: options.description
@@ -21172,6 +22737,100 @@ function registerMediaCommands(program2) {
21172
22737
  });
21173
22738
  }
21174
22739
  );
22740
+ const captions = media.command("captions").description("Caption (subtitle) tracks of a video - one per language");
22741
+ captions.command("list").description("List a video's caption tracks (language, source, status)").argument("<mediaId>", "video media item id").action(async (mediaId, _options, command) => {
22742
+ const flags = getGlobalFlags(command);
22743
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22744
+ const result = await client.listMediaCaptions(mediaId);
22745
+ emit(flags, result, () => {
22746
+ if (result.tracks.length === 0) {
22747
+ process.stdout.write(`No caption tracks on ${mediaId}.
22748
+ `);
22749
+ return;
22750
+ }
22751
+ printTable(
22752
+ result.tracks.map((track) => ({
22753
+ lang: track.srclang,
22754
+ label: track.label,
22755
+ source: track.source,
22756
+ status: track.recorded ? track.status : "not recorded",
22757
+ updated: track.updatedAt ?? ""
22758
+ })),
22759
+ ["lang", "label", "source", "status", "updated"]
22760
+ );
22761
+ });
22762
+ });
22763
+ captions.command("upload").description(
22764
+ "Add or replace the subtitles in one language from a .vtt or .srt file (UTF-8, up to 2 MB). The transcript's own language is refused - edit the transcript instead."
22765
+ ).argument("<mediaId>", "video media item id").requiredOption("--lang <code>", "language code of the track, e.g. en, de, pt-br").requiredOption("--file <path>", ".vtt or .srt file").option("--label <label>", "track name in the player's menu (default: the language name)").action(
22766
+ async (mediaId, options, command) => {
22767
+ const flags = getGlobalFlags(command);
22768
+ if (!existsSync(options.file)) {
22769
+ process.stderr.write(`File not found: ${options.file}
22770
+ `);
22771
+ process.exitCode = 1;
22772
+ return;
22773
+ }
22774
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22775
+ const result = await client.uploadMediaCaption(mediaId, {
22776
+ srclang: options.lang,
22777
+ content: readFileSync17(options.file, "utf8"),
22778
+ ...options.label ? { label: options.label } : {}
22779
+ });
22780
+ emit(flags, result, () => {
22781
+ process.stdout.write(
22782
+ result.track.status === "failed" ? `Stored the ${result.track.srclang} file, but the player refused it - run the same command again in a moment.
22783
+ ` : `Captions ${result.track.srclang} (${result.track.label}) are live on ${mediaId}.
22784
+ `
22785
+ );
22786
+ });
22787
+ }
22788
+ );
22789
+ captions.command("download").description("Print (or save) one caption track as WebVTT").argument("<mediaId>", "video media item id").requiredOption("--lang <code>", "language code of the track").option("--out <file>", "write the .vtt to a file instead of stdout").action(async (mediaId, options, command) => {
22790
+ const flags = getGlobalFlags(command);
22791
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22792
+ const result = await client.getMediaCaption(mediaId, options.lang);
22793
+ if (options.out) {
22794
+ writeFileSync4(options.out, result.content, "utf8");
22795
+ emit(flags, { mediaId, srclang: result.srclang, out: options.out }, () => {
22796
+ process.stdout.write(`Wrote the ${result.srclang} captions to ${options.out}.
22797
+ `);
22798
+ });
22799
+ return;
22800
+ }
22801
+ emit(flags, result, () => {
22802
+ process.stdout.write(result.content);
22803
+ });
22804
+ });
22805
+ captions.command("delete").description("Remove an uploaded caption track (it disappears from the player)").argument("<mediaId>", "video media item id").requiredOption("--lang <code>", "language code of the track").option("--confirm", "required: removes the track from the live player").action(
22806
+ async (mediaId, options, command) => {
22807
+ const flags = getGlobalFlags(command);
22808
+ if (!options.confirm) {
22809
+ process.stderr.write(
22810
+ "Refusing to delete without --confirm (the track disappears from the player).\n"
22811
+ );
22812
+ process.exitCode = 1;
22813
+ return;
22814
+ }
22815
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22816
+ const result = await client.deleteMediaCaption(mediaId, options.lang);
22817
+ emit(flags, result, () => {
22818
+ process.stdout.write(`Removed the ${result.srclang} captions from ${mediaId}.
22819
+ `);
22820
+ });
22821
+ }
22822
+ );
22823
+ captions.command("regenerate").description("Push the transcript's caption track to the player again").argument("<mediaId>", "video media item id").action(async (mediaId, _options, command) => {
22824
+ const flags = getGlobalFlags(command);
22825
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22826
+ const result = await client.regenerateMediaCaption(mediaId);
22827
+ emit(flags, result, () => {
22828
+ process.stdout.write(
22829
+ result.track.status === "failed" ? "The player refused the transcript captions - try again in a moment.\n" : `Transcript captions (${result.track.srclang}) pushed to the player.
22830
+ `
22831
+ );
22832
+ });
22833
+ });
21175
22834
  media.command("transcript").description(
21176
22835
  "Print a video transcript (plain text, or full JSON with --words / --json). Videos without a transcript yet report so; non-video media is a 404."
21177
22836
  ).argument("<mediaId>", "video media item id").option("--words", "include word-level timings (large payload; implies JSON output)").option("--out <file>", "write the transcript to a file instead of stdout").action(
@@ -21223,7 +22882,7 @@ function registerMediaCommands(program2) {
21223
22882
  ).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(
21224
22883
  async (options, command) => {
21225
22884
  const flags = getGlobalFlags(command);
21226
- requireConfirm7(options.confirm, "generate an AI image (spends AI credits)");
22885
+ requireConfirm10(options.confirm, "generate an AI image (spends AI credits)");
21227
22886
  const input = {
21228
22887
  prompt: resolveTextOrFile(options.prompt),
21229
22888
  aspectRatio: parseEnumOption(
@@ -21255,7 +22914,7 @@ function registerMediaCommands(program2) {
21255
22914
  ).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(
21256
22915
  async (mediaId, options, command) => {
21257
22916
  const flags = getGlobalFlags(command);
21258
- requireConfirm7(options.confirm, `transcribe ${mediaId} (spends transcription minutes)`);
22917
+ requireConfirm10(options.confirm, `transcribe ${mediaId} (spends transcription minutes)`);
21259
22918
  const input = {};
21260
22919
  if (options.language !== void 0) input.language = options.language;
21261
22920
  if (options.force) input.force = true;
@@ -21329,13 +22988,13 @@ function registerMediaCommands(program2) {
21329
22988
  }
21330
22989
 
21331
22990
  // src/commands/newsletters/index.ts
21332
- import { existsSync as existsSync2, readFileSync as readFileSync16 } from "fs";
22991
+ import { existsSync as existsSync2, readFileSync as readFileSync18 } from "fs";
21333
22992
  var ISSUE_STATUSES = ["public", "hidden"];
21334
22993
  var ISSUE_VISIBILITIES = ["public", "hidden", "locked"];
21335
22994
  var ISSUE_RENDER_MODES = ["snapshot", "article"];
21336
22995
  var THUMBNAIL_NONE = "none";
21337
22996
  function resolveMarkdownOrFile(value) {
21338
- return existsSync2(value) ? readFileSync16(value, "utf8") : value;
22997
+ return existsSync2(value) ? readFileSync18(value, "utf8") : value;
21339
22998
  }
21340
22999
  function parseIdList2(value, flag) {
21341
23000
  const ids = value.split(",").map((id) => id.trim()).filter(Boolean);
@@ -21348,6 +23007,7 @@ var ACCESS_STRATEGIES = [
21348
23007
  "from_subscribe_date"
21349
23008
  ];
21350
23009
  var DOUBLE_OPT_INS = ["none", "external"];
23010
+ var DELIVERIES = ["zanfia", "integration"];
21351
23011
  function assertOneOf2(value, allowed, flag) {
21352
23012
  if (!allowed.includes(value)) {
21353
23013
  throw new Error(`Invalid ${flag}: ${value}. Use one of: ${allowed.join(", ")}`);
@@ -21376,7 +23036,7 @@ function parseBool2(value, flag) {
21376
23036
  }
21377
23037
  function parseJsonFileOrInline5(value, flag) {
21378
23038
  const trimmed = value.trim();
21379
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync16(value, "utf8");
23039
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync18(value, "utf8");
21380
23040
  try {
21381
23041
  return JSON.parse(raw);
21382
23042
  } catch (error) {
@@ -21405,9 +23065,10 @@ function registerNewslettersCommands(program2) {
21405
23065
  slug: issue.slug,
21406
23066
  status: issue.status,
21407
23067
  landing: issue.landingPageVisibility ?? "",
23068
+ send: issue.sendStatus ?? "",
21408
23069
  createdAt: issue.createdAt
21409
23070
  })),
21410
- ["id", "subject", "slug", "status", "landing", "createdAt"]
23071
+ ["id", "subject", "slug", "status", "landing", "send", "createdAt"]
21411
23072
  );
21412
23073
  if (page.nextCursor) {
21413
23074
  process.stdout.write(`
@@ -21591,6 +23252,90 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
21591
23252
  });
21592
23253
  }
21593
23254
  );
23255
+ issues.command("send").description(
23256
+ "\u26A0\uFE0F SENDS the issue from Zanfia to every subscriber of the newsletter product \u2014 REAL bulk e-mail, IRREVERSIBLE once delivered (`issues cancel` only stops the remaining sends). Sending is publishing: a staged draft is promoted first. The newsletter must send from Zanfia (`settings update --delivery zanfia`). Requires --confirm. Omit --at to send NOW; --at schedules (max 30 days out)."
23257
+ ).argument("<productId>", "newsletter product id").argument("<issueId>", "issue id").option(
23258
+ "--at <isoTimestamp>",
23259
+ "schedule the send (ISO, max 30 days out) instead of sending now"
23260
+ ).option("--confirm", "required \u2014 acknowledge this e-mails the subscribers for real").action(
23261
+ async (productId, issueId, options, command) => {
23262
+ const flags = getGlobalFlags(command);
23263
+ if (!options.confirm) {
23264
+ throw new Error(
23265
+ `Refusing to send without --confirm. \`newsletters issues send\` e-mails EVERY subscriber of the newsletter for real and cannot be undone once mail is delivered. Re-run with --confirm to proceed (check the issue first with \`newsletters issues list --product ${productId}\`).`
23266
+ );
23267
+ }
23268
+ if (options.at !== void 0 && Number.isNaN(Date.parse(options.at))) {
23269
+ throw new Error(
23270
+ `Invalid --at timestamp: ${options.at} (expected ISO, e.g. 2026-07-15T09:00:00Z)`
23271
+ );
23272
+ }
23273
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23274
+ const result = await client.sendNewsletterIssue(productId, issueId, {
23275
+ ...options.at !== void 0 ? { scheduledAt: options.at } : {}
23276
+ });
23277
+ emit(flags, result, () => {
23278
+ const { send } = result;
23279
+ if (send.scheduledAt) {
23280
+ process.stdout.write(
23281
+ `Issue ${result.issueId} scheduled for ${send.scheduledAt} \u2014 stop it before then with \`newsletters issues cancel ${productId} ${issueId} --confirm\`.
23282
+ `
23283
+ );
23284
+ } else {
23285
+ process.stdout.write(
23286
+ `Issue ${result.issueId} is sending now (status: ${send.status}). \`newsletters issues cancel\` can only stop the remaining sends.
23287
+ `
23288
+ );
23289
+ }
23290
+ });
23291
+ }
23292
+ );
23293
+ issues.command("cancel").description(
23294
+ "Cancel a scheduled or in-flight issue send \u2014 remaining sends stop; delivered mail is out. Requires --confirm."
23295
+ ).argument("<productId>", "newsletter product id").argument("<issueId>", "issue id").option("--confirm", "required \u2014 acknowledge the send is stopped").action(
23296
+ async (productId, issueId, options, command) => {
23297
+ const flags = getGlobalFlags(command);
23298
+ if (!options.confirm) {
23299
+ throw new Error(
23300
+ "Refusing to cancel without --confirm. Re-run with --confirm to un-queue the scheduled send or stop the one in flight."
23301
+ );
23302
+ }
23303
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23304
+ const result = await client.cancelNewsletterIssueSend(productId, issueId);
23305
+ emit(flags, result, () => {
23306
+ process.stdout.write(
23307
+ result.send ? `Stopped the send of issue ${result.issueId} (status: ${result.send.status}).
23308
+ ` : `Un-queued the send of issue ${result.issueId}; it is a draft again.
23309
+ `
23310
+ );
23311
+ });
23312
+ }
23313
+ );
23314
+ issues.command("report").description("Progress and delivery / engagement stats of an issue\u2019s native send").argument("<productId>", "newsletter product id").argument("<issueId>", "issue id").action(async (productId, issueId, command) => {
23315
+ const flags = getGlobalFlags(command);
23316
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23317
+ const result = await client.getNewsletterIssueSendReport(productId, issueId);
23318
+ emit(flags, result, () => {
23319
+ if (!result.send) {
23320
+ process.stdout.write(`Issue ${result.issueId} was never sent from Zanfia.
23321
+ `);
23322
+ return;
23323
+ }
23324
+ const { send, report } = result;
23325
+ process.stdout.write(
23326
+ `Issue ${result.issueId}: ${send.status}` + (send.scheduledAt ? ` (scheduled for ${send.scheduledAt})` : "") + (send.recipientCount !== null ? `, ${send.recipientCount} recipients` : "") + "\n"
23327
+ );
23328
+ if (report) {
23329
+ printTable(
23330
+ Object.entries(report.stats).map(([stat4, value]) => ({
23331
+ stat: stat4,
23332
+ value: String(value ?? "")
23333
+ })),
23334
+ ["stat", "value"]
23335
+ );
23336
+ }
23337
+ });
23338
+ });
21594
23339
  issues.command("delete").description(
21595
23340
  "\u26A0\uFE0F PERMANENTLY delete an archived issue \u2014 a HARD delete with no undo. Requires --confirm."
21596
23341
  ).argument("<issueId>", "archived issue id").requiredOption("--product <id>", "newsletter product id").option("--confirm", "required \u2014 acknowledge the issue is removed permanently").action(
@@ -21609,9 +23354,11 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
21609
23354
  });
21610
23355
  }
21611
23356
  );
21612
- const settings = newsletters.command("settings").description("Edit newsletter settings (archive, archive inbox, double opt-in, client tags)");
23357
+ const settings = newsletters.command("settings").description(
23358
+ "Edit newsletter settings (archive, archive inbox, double opt-in, client tags, delivery, sender)"
23359
+ );
21613
23360
  settings.command("update").description(
21614
- "Patch newsletter settings. Archive flags merge per-field; --archive-inbox, --double-opt-in and --client-tag replace their whole block. At least one change is required."
23361
+ "Patch newsletter settings. Archive and sender flags merge per-field; --archive-inbox, --double-opt-in and --client-tag replace their whole block. At least one change is required."
21615
23362
  ).requiredOption("--product <id>", "newsletter product id").option("--archive-enabled <on|off>", "archive: enable/disable the public archive").option("--archive-access <strategy>", `archive: ${ACCESS_STRATEGIES.join("|")}`).option(
21616
23363
  "--publication-delay <days|none>",
21617
23364
  'archive: days before a new issue goes public; "none" = immediately'
@@ -21629,9 +23376,18 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
21629
23376
  "tag applied to new subscribers (repeat the flag; replaces the list)",
21630
23377
  collectRepeatable4,
21631
23378
  []
21632
- ).action(
23379
+ ).option(
23380
+ "--delivery <mode>",
23381
+ `who sends the issues: ${DELIVERIES.join("|")} (zanfia = native sending from the product)`
23382
+ ).option(
23383
+ "--from-name <name>",
23384
+ 'sender: From display name of native sends ("" = workspace default)'
23385
+ ).option("--reply-to <email>", 'sender: Reply-To of native sends ("" = workspace default)').option("--clear-sender", "sender: drop both defaults (workspace defaults apply)").action(
21633
23386
  async (options, command) => {
21634
23387
  const flags = getGlobalFlags(command);
23388
+ if (options.clearSender && (options.fromName !== void 0 || options.replyTo !== void 0)) {
23389
+ throw new Error("Use either --clear-sender or --from-name/--reply-to, not both.");
23390
+ }
21635
23391
  const archive = {};
21636
23392
  if (options.archiveEnabled !== void 0) {
21637
23393
  archive.enabled = parseBool2(options.archiveEnabled, "--archive-enabled");
@@ -21675,6 +23431,17 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
21675
23431
  input.doubleOptIn = assertOneOf2(options.doubleOptIn, DOUBLE_OPT_INS, "--double-opt-in");
21676
23432
  }
21677
23433
  if (options.clientTag.length > 0) input.clientTags = options.clientTag;
23434
+ if (options.delivery !== void 0) {
23435
+ input.delivery = assertOneOf2(options.delivery, DELIVERIES, "--delivery");
23436
+ }
23437
+ if (options.clearSender) {
23438
+ input.sender = null;
23439
+ } else if (options.fromName !== void 0 || options.replyTo !== void 0) {
23440
+ input.sender = {
23441
+ ...options.fromName !== void 0 ? { fromName: options.fromName || null } : {},
23442
+ ...options.replyTo !== void 0 ? { replyTo: options.replyTo || null } : {}
23443
+ };
23444
+ }
21678
23445
  if (Object.keys(input).length === 0) {
21679
23446
  throw new Error(
21680
23447
  "Nothing to update \u2014 pass at least one settings flag (see `newsletters settings update --help`)."
@@ -21691,7 +23458,7 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
21691
23458
  }
21692
23459
 
21693
23460
  // src/commands/offers/index.ts
21694
- import { readFileSync as readFileSync17 } from "fs";
23461
+ import { readFileSync as readFileSync19 } from "fs";
21695
23462
  var PRICE_BILLING_TYPES = ["one_time", "recurring", "installments"];
21696
23463
  var PRICE_INTERVALS = ["day", "week", "month", "year"];
21697
23464
  var SALE_START_DISPLAYS = ["hidden", "countdown"];
@@ -21701,36 +23468,166 @@ var PRICE_OPERATIONS = [
21701
23468
  "deactivate",
21702
23469
  "hide",
21703
23470
  "unhide",
21704
- "change-amount"
23471
+ "change-amount",
23472
+ "edit-details"
21705
23473
  ];
23474
+ var EDIT_DETAILS_OPERATION = "edit-details";
23475
+ var CHANGE_AMOUNT_OPERATION = "change-amount";
21706
23476
  function parseChoice4(value, allowed, flag) {
21707
23477
  const match = allowed.find((candidate) => candidate === value);
21708
23478
  if (match === void 0) {
21709
23479
  throw new Error(`Invalid ${flag}: ${value} (expected one of: ${allowed.join(", ")})`);
21710
23480
  }
21711
- return match;
21712
- }
21713
- function parsePositiveInt12(value, flag) {
21714
- const n = Number.parseInt(value, 10);
21715
- if (!Number.isInteger(n) || n <= 0) {
21716
- throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
23481
+ return match;
23482
+ }
23483
+ function parsePositiveInt12(value, flag) {
23484
+ const n = Number.parseInt(value, 10);
23485
+ if (!Number.isInteger(n) || n <= 0) {
23486
+ throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
23487
+ }
23488
+ return n;
23489
+ }
23490
+ function parseBool3(value, flag) {
23491
+ const v = value.trim().toLowerCase();
23492
+ if (["on", "true", "yes", "1"].includes(v)) return true;
23493
+ if (["off", "false", "no", "0"].includes(v)) return false;
23494
+ throw new Error(`Invalid ${flag}: ${value} (expected on/off, true/false, yes/no).`);
23495
+ }
23496
+ function parseJsonFileOrInline6(value, flag) {
23497
+ const trimmed = value.trim();
23498
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync19(value, "utf8");
23499
+ try {
23500
+ return JSON.parse(raw);
23501
+ } catch (error) {
23502
+ throw new Error(`Invalid JSON for ${flag}: ${error.message}`);
23503
+ }
23504
+ }
23505
+ function buildPriceUpdateInput(options) {
23506
+ const hasDetails = options.label !== void 0 || options.description !== void 0;
23507
+ const operation = options.operation !== void 0 ? parseChoice4(options.operation, PRICE_OPERATIONS, "--operation") : hasDetails ? EDIT_DETAILS_OPERATION : void 0;
23508
+ if (operation === void 0) {
23509
+ throw new Error(
23510
+ `--operation <${PRICE_OPERATIONS.join("|")}> is required (or pass --label / --description to edit the details).`
23511
+ );
23512
+ }
23513
+ if (hasDetails && operation !== EDIT_DETAILS_OPERATION) {
23514
+ throw new Error(
23515
+ `--label / --description only apply to --operation ${EDIT_DETAILS_OPERATION}, not ${operation}.`
23516
+ );
23517
+ }
23518
+ if (operation === EDIT_DETAILS_OPERATION) {
23519
+ if (!hasDetails) {
23520
+ throw new Error(
23521
+ `--operation ${EDIT_DETAILS_OPERATION} requires --label and/or --description.`
23522
+ );
23523
+ }
23524
+ if (options.label !== void 0 && options.label.trim().length === 0) {
23525
+ throw new Error("--label must not be empty (labels cannot be cleared, only replaced).");
23526
+ }
23527
+ return {
23528
+ operation,
23529
+ newLabel: options.label,
23530
+ // An empty string clears the description (the API accepts '' or null; null is explicit).
23531
+ newDescription: options.description === void 0 ? void 0 : options.description.length === 0 ? null : options.description
23532
+ };
23533
+ }
23534
+ if (operation === CHANGE_AMOUNT_OPERATION && !options.amount) {
23535
+ throw new Error(`--operation ${CHANGE_AMOUNT_OPERATION} requires --amount <minor-units>.`);
23536
+ }
23537
+ return {
23538
+ operation,
23539
+ newAmountCents: operation === CHANGE_AMOUNT_OPERATION && options.amount ? parsePositiveInt12(options.amount, "--amount") : void 0
23540
+ };
23541
+ }
23542
+ var MEDIA_TYPES = ["image", "video"];
23543
+ var IMAGE_MEDIA_TYPE2 = "image";
23544
+ var VIDEO_MEDIA_TYPE = "video";
23545
+ var READY_UPLOAD_STATUS = "ready";
23546
+ var GALLERY_KEYS = /* @__PURE__ */ new Set([
23547
+ "checkoutMedia",
23548
+ "checkoutImages",
23549
+ "checkoutImagesMedia"
23550
+ ]);
23551
+ var BASE_DETAIL_KEYS = /* @__PURE__ */ new Set([
23552
+ "id",
23553
+ "productId",
23554
+ "name",
23555
+ "slug",
23556
+ "title",
23557
+ "active",
23558
+ "archived",
23559
+ "isFree",
23560
+ "priceCount",
23561
+ "createdAt",
23562
+ "description",
23563
+ "language",
23564
+ "successUrl",
23565
+ "prices",
23566
+ "orderBumps"
23567
+ ]);
23568
+ function galleryOf(checkout) {
23569
+ if (checkout.checkoutMedia) return [...checkout.checkoutMedia];
23570
+ return (checkout.checkoutImages ?? []).map((url, index) => ({
23571
+ type: IMAGE_MEDIA_TYPE2,
23572
+ url,
23573
+ mediaId: checkout.checkoutImagesMedia?.[index]?.mediaId ?? null,
23574
+ thumbnailUrl: null
23575
+ }));
23576
+ }
23577
+ function galleryPatch(media) {
23578
+ const images = media.filter((item) => item.type === IMAGE_MEDIA_TYPE2);
23579
+ return {
23580
+ checkoutMedia: media,
23581
+ checkoutImages: images.map((item) => item.url),
23582
+ checkoutImagesMedia: images.map((item) => ({ url: item.url, mediaId: item.mediaId }))
23583
+ };
23584
+ }
23585
+ function bunnyEmbedUrl(video) {
23586
+ return `https://iframe.mediadelivery.net/embed/${video.bunnyLibraryId}/${video.bunnyVideoId}`;
23587
+ }
23588
+ function galleryEntryFromLibrary(item) {
23589
+ const type = MEDIA_TYPES.find((candidate) => candidate === item.type);
23590
+ if (type === void 0) {
23591
+ throw new Error(
23592
+ `Media ${item.id} is a ${item.type} \u2014 only images and videos can go in the offer gallery.`
23593
+ );
23594
+ }
23595
+ if (type === VIDEO_MEDIA_TYPE) {
23596
+ if (item.uploadStatus !== READY_UPLOAD_STATUS || !item.video) {
23597
+ throw new Error(
23598
+ `Media ${item.id} is a video that is not ready yet (uploadStatus: ${item.uploadStatus}) \u2014 add it once it finishes processing.`
23599
+ );
23600
+ }
23601
+ return {
23602
+ type,
23603
+ url: bunnyEmbedUrl(item.video),
23604
+ mediaId: item.id,
23605
+ thumbnailUrl: item.thumbnailUrl ?? null
23606
+ };
23607
+ }
23608
+ if (!item.url) {
23609
+ throw new Error(`Media ${item.id} has no public URL yet (uploadStatus: ${item.uploadStatus}).`);
21717
23610
  }
21718
- return n;
23611
+ return { type, url: item.url, mediaId: item.id, thumbnailUrl: item.thumbnailUrl ?? null };
21719
23612
  }
21720
- function parseBool3(value, flag) {
21721
- const v = value.trim().toLowerCase();
21722
- if (["on", "true", "yes", "1"].includes(v)) return true;
21723
- if (["off", "false", "no", "0"].includes(v)) return false;
21724
- throw new Error(`Invalid ${flag}: ${value} (expected on/off, true/false, yes/no).`);
23613
+ function printGallery(media) {
23614
+ printTable(
23615
+ media.map((item, index) => ({
23616
+ index,
23617
+ type: item.type,
23618
+ mediaId: item.mediaId ?? "",
23619
+ url: item.url,
23620
+ thumbnailUrl: item.thumbnailUrl ?? ""
23621
+ })),
23622
+ ["index", "type", "mediaId", "url", "thumbnailUrl"]
23623
+ );
21725
23624
  }
21726
- function parseJsonFileOrInline6(value, flag) {
21727
- const trimmed = value.trim();
21728
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync17(value, "utf8");
21729
- try {
21730
- return JSON.parse(raw);
21731
- } catch (error) {
21732
- throw new Error(`Invalid JSON for ${flag}: ${error.message}`);
21733
- }
23625
+ function fullSettingsOf(checkout) {
23626
+ const isRendered = (key) => {
23627
+ const known = key;
23628
+ return BASE_DETAIL_KEYS.has(known) || GALLERY_KEYS.has(known);
23629
+ };
23630
+ return Object.fromEntries(Object.entries(checkout).filter(([key]) => !isRendered(key)));
21734
23631
  }
21735
23632
  function formatAmount2(amountCents, currency) {
21736
23633
  return `${(amountCents / 100).toFixed(2)} ${currency}`;
@@ -21984,10 +23881,12 @@ function registerOffersCommands(program2) {
21984
23881
  );
21985
23882
  });
21986
23883
  });
21987
- offers.command("get").description("Get a single offer, including its prices and attached order bumps").argument("<checkoutId>", "offer (checkout) id").action(async (checkoutId, _, command) => {
23884
+ offers.command("get").description(
23885
+ "Get a single offer, including its prices and attached order bumps. --full adds every editable setting (media gallery, theme, deadline, afterPurchase, oto chain, form, legal, invoicing, notifications, tracking) \u2014 the same keys `offers update --json` accepts"
23886
+ ).argument("<checkoutId>", "offer (checkout) id").option("--full", "include the whole editable settings surface (?include=full)").action(async (checkoutId, options, command) => {
21988
23887
  const flags = getGlobalFlags(command);
21989
23888
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21990
- const checkout = await client.getCheckout(checkoutId);
23889
+ const checkout = await client.getCheckout(checkoutId, { full: options.full === true });
21991
23890
  emit(flags, checkout, () => {
21992
23891
  process.stdout.write(`Offer ${checkout.id} \u2014 ${checkout.name}
21993
23892
  `);
@@ -22047,6 +23946,14 @@ Order bumps (${checkout.orderBumps.length}):
22047
23946
  process.stdout.write(` ${bump.orderBumpId} (${scope})
22048
23947
  `);
22049
23948
  }
23949
+ if (!options.full) return;
23950
+ const gallery = galleryOf(checkout);
23951
+ process.stdout.write(`
23952
+ Media gallery (${gallery.length}):
23953
+ `);
23954
+ printGallery(gallery);
23955
+ process.stdout.write("\nSettings (--full):\n");
23956
+ printJson(fullSettingsOf(checkout));
22050
23957
  });
22051
23958
  });
22052
23959
  offers.command("create").description("Create a new offer for a product").requiredOption("--product <productId>", "product the offer sells").option("--free", "create a free offer (cannot carry prices)").option("--name <name>", "offer name").action(
@@ -22255,21 +24162,23 @@ Order bumps (${checkout.orderBumps.length}):
22255
24162
  );
22256
24163
  });
22257
24164
  });
22258
- price.command("update").description("Mutate a price variant (activate, deactivate, hide, unhide, change-amount)").argument("<checkoutId>", "offer (checkout) id").argument("<priceId>", "price id").requiredOption("--operation <op>", "activate | deactivate | hide | unhide | change-amount").option("--amount <minor-units>", "change-amount: new price in minor units, e.g. 1999 = 19.99").action(
24165
+ price.command("update").description(
24166
+ "Mutate a price variant: --operation activate | deactivate | hide | unhide | change-amount (with --amount) | edit-details (with --label / --description). --label / --description alone imply edit-details"
24167
+ ).argument("<checkoutId>", "offer (checkout) id").argument("<priceId>", "price id").option(
24168
+ "--operation <op>",
24169
+ "activate | deactivate | hide | unhide | change-amount | edit-details (implied by --label / --description)"
24170
+ ).option("--amount <minor-units>", "change-amount: new price in minor units, e.g. 1999 = 19.99").option("--label <text>", "edit-details: new display label").option(
24171
+ "--description <markdown>",
24172
+ "edit-details: new markdown description under the price (empty string clears it)"
24173
+ ).action(
22259
24174
  async (checkoutId, priceId, options, command) => {
22260
24175
  const flags = getGlobalFlags(command);
22261
- const operation = parseChoice4(options.operation, PRICE_OPERATIONS, "--operation");
22262
- if (operation === "change-amount" && !options.amount) {
22263
- throw new Error("--operation change-amount requires --amount <minor-units>.");
22264
- }
24176
+ const input = buildPriceUpdateInput(options);
22265
24177
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22266
- const result = await client.updateCheckoutPrice(checkoutId, priceId, {
22267
- operation,
22268
- newAmountCents: operation === "change-amount" && options.amount ? parsePositiveInt12(options.amount, "--amount") : void 0
22269
- });
24178
+ const result = await client.updateCheckoutPrice(checkoutId, priceId, input);
22270
24179
  emit(flags, result, () => {
22271
24180
  process.stdout.write(
22272
- `Applied ${operation} to price ${result.priceId} on offer ${result.checkoutId}
24181
+ `Applied ${input.operation} to price ${result.priceId} on offer ${result.checkoutId}
22273
24182
  `
22274
24183
  );
22275
24184
  });
@@ -22305,6 +24214,87 @@ Order bumps (${checkout.orderBumps.length}):
22305
24214
  `);
22306
24215
  });
22307
24216
  });
24217
+ const media = offers.command("media").description("Manage an offer's media gallery (checkout images and videos)");
24218
+ media.command("list").description("List the offer's gallery entries in display order (index 0 is featured)").argument("<checkoutId>", "offer (checkout) id").action(async (checkoutId, _, command) => {
24219
+ const flags = getGlobalFlags(command);
24220
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24221
+ const checkout = await client.getCheckout(checkoutId, { full: true });
24222
+ const gallery = galleryOf(checkout);
24223
+ emit(flags, { checkoutId: checkout.id, count: gallery.length, media: gallery }, () => {
24224
+ process.stdout.write(`Media gallery of offer ${checkout.id} (${gallery.length}):
24225
+ `);
24226
+ printGallery(gallery);
24227
+ });
24228
+ });
24229
+ media.command("add").description(
24230
+ "Append a Media Library item (--media <mediaId>; videos must be ready) or an external URL (--url with --type) to the gallery"
24231
+ ).argument("<checkoutId>", "offer (checkout) id").option("--media <mediaId>", "Media Library item id (see `media list`)").option("--url <url>", "external image URL or video iframe embed URL (needs --type)").option("--type <image|video>", "with --url: what the URL renders as").option("--thumbnail <url>", "with --url: poster/thumbnail URL for the strip").action(
24232
+ async (checkoutId, options, command) => {
24233
+ if (options.media === void 0 === (options.url === void 0)) {
24234
+ throw new Error("Pass exactly one of --media <mediaId> or --url <url>.");
24235
+ }
24236
+ if (options.media !== void 0 && (options.type !== void 0 || options.thumbnail)) {
24237
+ throw new Error(
24238
+ "--type / --thumbnail only apply with --url (a library item carries its own)."
24239
+ );
24240
+ }
24241
+ const flags = getGlobalFlags(command);
24242
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24243
+ let entry;
24244
+ if (options.media !== void 0) {
24245
+ const { media: item } = await client.getMedia(options.media);
24246
+ entry = galleryEntryFromLibrary(item);
24247
+ } else {
24248
+ if (options.type === void 0) {
24249
+ throw new Error("--url requires --type <image|video>.");
24250
+ }
24251
+ entry = {
24252
+ type: parseChoice4(options.type, MEDIA_TYPES, "--type"),
24253
+ url: options.url ?? "",
24254
+ mediaId: null,
24255
+ thumbnailUrl: options.thumbnail ?? null
24256
+ };
24257
+ }
24258
+ const checkout = await client.getCheckout(checkoutId, { full: true });
24259
+ const gallery = galleryOf(checkout);
24260
+ const duplicate = gallery.find(
24261
+ (item) => entry.mediaId !== null && item.mediaId === entry.mediaId || item.url === entry.url
24262
+ );
24263
+ if (duplicate) {
24264
+ throw new Error(
24265
+ `Already in the gallery: ${duplicate.mediaId ?? duplicate.url} (index ${gallery.indexOf(duplicate)}).`
24266
+ );
24267
+ }
24268
+ const next = [...gallery, entry];
24269
+ const result = await client.updateCheckout(checkoutId, galleryPatch(next));
24270
+ emit(flags, { checkoutId: result.checkoutId, added: entry, media: next }, () => {
24271
+ process.stdout.write(
24272
+ `Added ${entry.type} ${entry.mediaId ?? entry.url} to offer ${result.checkoutId} (gallery: ${next.length})
24273
+ `
24274
+ );
24275
+ });
24276
+ }
24277
+ );
24278
+ media.command("remove").description("Remove a gallery entry by its media id or URL").argument("<checkoutId>", "offer (checkout) id").argument("<mediaIdOrUrl>", "media id or URL of the entry to drop (see `offers media list`)").action(async (checkoutId, ref, _, command) => {
24279
+ const flags = getGlobalFlags(command);
24280
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24281
+ const checkout = await client.getCheckout(checkoutId, { full: true });
24282
+ const gallery = galleryOf(checkout);
24283
+ const removed = gallery.filter((item) => item.mediaId === ref || item.url === ref);
24284
+ if (removed.length === 0) {
24285
+ throw new Error(
24286
+ `No gallery entry matches ${ref} on offer ${checkoutId} \u2014 run \`offers media list ${checkoutId}\`.`
24287
+ );
24288
+ }
24289
+ const next = gallery.filter((item) => !removed.includes(item));
24290
+ const result = await client.updateCheckout(checkoutId, galleryPatch(next));
24291
+ emit(flags, { checkoutId: result.checkoutId, removed, media: next }, () => {
24292
+ process.stdout.write(
24293
+ `Removed ${removed.length} ${removed.length === 1 ? "entry" : "entries"} (${ref}) from offer ${result.checkoutId} (gallery: ${next.length})
24294
+ `
24295
+ );
24296
+ });
24297
+ });
22308
24298
  }
22309
24299
 
22310
24300
  // src/commands/orders/index.ts
@@ -22326,7 +24316,7 @@ function parsePositiveInt13(value, flag) {
22326
24316
  function formatAmount3(amountCents, currency) {
22327
24317
  return `${(amountCents / 100).toFixed(2)} ${currency}`;
22328
24318
  }
22329
- function requireConfirm8(confirm, what) {
24319
+ function requireConfirm11(confirm, what) {
22330
24320
  if (!confirm) {
22331
24321
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
22332
24322
  }
@@ -22425,7 +24415,7 @@ Next page: --cursor ${page.nextCursor}
22425
24415
  ).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(
22426
24416
  async (orderId, options, command) => {
22427
24417
  const flags = getGlobalFlags(command);
22428
- requireConfirm8(options.confirm, `approve order ${orderId} (access + purchase email)`);
24418
+ requireConfirm11(options.confirm, `approve order ${orderId} (access + purchase email)`);
22429
24419
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22430
24420
  const result = await client.approveManualOrder(orderId, {
22431
24421
  issueInvoice: options.issueInvoice === true ? true : void 0
@@ -22442,7 +24432,7 @@ Next page: --cursor ${page.nextCursor}
22442
24432
  "Reject a manual (wire-transfer) order \u2014 cancels it and notifies the buyer. 409 = the order was already decided. Requires --confirm"
22443
24433
  ).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) => {
22444
24434
  const flags = getGlobalFlags(command);
22445
- requireConfirm8(options.confirm, `reject order ${orderId} (cancel + notify the buyer)`);
24435
+ requireConfirm11(options.confirm, `reject order ${orderId} (cancel + notify the buyer)`);
22446
24436
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22447
24437
  const result = await client.rejectManualOrder(orderId);
22448
24438
  emit(flags, result, () => {
@@ -22492,12 +24482,12 @@ Next page: --cursor ${page.nextCursor}
22492
24482
  }
22493
24483
 
22494
24484
  // src/commands/oto/index.ts
22495
- import { readFileSync as readFileSync18 } from "fs";
24485
+ import { readFileSync as readFileSync20 } from "fs";
22496
24486
  var MIN_TIMER_MINUTES = 1;
22497
24487
  var MAX_TIMER_MINUTES = 120;
22498
24488
  function parseJsonFileOrInline7(value, flag) {
22499
24489
  const trimmed = value.trim();
22500
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync18(value, "utf8");
24490
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync20(value, "utf8");
22501
24491
  try {
22502
24492
  return JSON.parse(raw);
22503
24493
  } catch (error) {
@@ -22685,7 +24675,7 @@ function registerOtoCommands(program2) {
22685
24675
  }
22686
24676
 
22687
24677
  // src/commands/pages/index.ts
22688
- import { existsSync as existsSync3, readFileSync as readFileSync19 } from "fs";
24678
+ import { existsSync as existsSync3, readFileSync as readFileSync21 } from "fs";
22689
24679
  import { readFile as readFile2 } from "fs/promises";
22690
24680
  async function resolveHtml2(options) {
22691
24681
  if (options.file) {
@@ -22732,13 +24722,13 @@ function parseCsv4(value) {
22732
24722
  ];
22733
24723
  }
22734
24724
  function resolveTextOrFile2(value) {
22735
- const text = existsSync3(value) ? readFileSync19(value, "utf8") : value;
24725
+ const text = existsSync3(value) ? readFileSync21(value, "utf8") : value;
22736
24726
  if (!text.trim()) throw new Error("The text (or the file it points to) is empty.");
22737
24727
  return text;
22738
24728
  }
22739
24729
  function parseJsonObjectFileOrInline3(value, flag) {
22740
24730
  const trimmed = value.trim();
22741
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync19(value, "utf8");
24731
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync21(value, "utf8");
22742
24732
  let parsed;
22743
24733
  try {
22744
24734
  parsed = JSON.parse(raw);
@@ -22750,7 +24740,7 @@ function parseJsonObjectFileOrInline3(value, flag) {
22750
24740
  }
22751
24741
  return parsed;
22752
24742
  }
22753
- function requireConfirm9(confirm, what) {
24743
+ function requireConfirm12(confirm, what) {
22754
24744
  if (!confirm) {
22755
24745
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
22756
24746
  }
@@ -22945,7 +24935,7 @@ function registerPagesCommands(program2) {
22945
24935
  "inline JSON object or .json path merged into the request body (explicit flags win)"
22946
24936
  ).option("--confirm", "required \u2014 acknowledge that AI credits will be consumed").action(async (options, command) => {
22947
24937
  const flags = getGlobalFlags(command);
22948
- requireConfirm9(options.confirm, "generate a page (consumes AI credits)");
24938
+ requireConfirm12(options.confirm, "generate a page (consumes AI credits)");
22949
24939
  const input = buildGenerateInput(options);
22950
24940
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22951
24941
  const result = await client.generatePage(input).catch(withSchedulerHint3);
@@ -22967,7 +24957,7 @@ function registerPagesCommands(program2) {
22967
24957
  ).action(
22968
24958
  async (pageId, options, command) => {
22969
24959
  const flags = getGlobalFlags(command);
22970
- requireConfirm9(
24960
+ requireConfirm12(
22971
24961
  options.confirm,
22972
24962
  "AI-edit the page (consumes AI credits, replaces the HTML)"
22973
24963
  );
@@ -23026,7 +25016,7 @@ function registerPagesCommands(program2) {
23026
25016
  ).argument("<pageId>", "page id").argument("<revisionId>", "revision id from `pages revisions`").option("--confirm", "required \u2014 acknowledge that the current HTML will be replaced").action(
23027
25017
  async (pageId, revisionId, options, command) => {
23028
25018
  const flags = getGlobalFlags(command);
23029
- requireConfirm9(options.confirm, "restore the revision (replaces the current HTML)");
25019
+ requireConfirm12(options.confirm, "restore the revision (replaces the current HTML)");
23030
25020
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23031
25021
  const result = await client.restorePageRevision(pageId, revisionId);
23032
25022
  emit(flags, result, () => {
@@ -23039,7 +25029,7 @@ function registerPagesCommands(program2) {
23039
25029
  "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."
23040
25030
  ).argument("<pageId>", "page id").option("--confirm", "required \u2014 acknowledge that automations go live and emails will send").action(async (pageId, options, command) => {
23041
25031
  const flags = getGlobalFlags(command);
23042
- requireConfirm9(options.confirm, "activate the page automations (emails will send)");
25032
+ requireConfirm12(options.confirm, "activate the page automations (emails will send)");
23043
25033
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23044
25034
  const result = await client.activatePageAutomations(pageId);
23045
25035
  emit(flags, result, () => {
@@ -23069,7 +25059,9 @@ function registerPagesCommands(program2) {
23069
25059
  }
23070
25060
 
23071
25061
  // src/commands/products/index.ts
23072
- import { readFileSync as readFileSync20 } from "fs";
25062
+ import { readFileSync as readFileSync22 } from "fs";
25063
+ import { readFile as readFile3 } from "fs/promises";
25064
+ import { basename as basename2 } from "path";
23073
25065
  var KB_TREE_CHANGED_ERROR = "tree-changed";
23074
25066
  var JSON_FLAG_HINT3 = "(write it as --json=<value> \u2014 a bare --json is the global JSON-output flag)";
23075
25067
  function parseCoverRef(value) {
@@ -23175,7 +25167,7 @@ function assertOneOf3(value, allowed, flag) {
23175
25167
  }
23176
25168
  function parseJsonFileOrInline8(value, flag) {
23177
25169
  const trimmed = value.trim();
23178
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync20(value, "utf8");
25170
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync22(value, "utf8");
23179
25171
  try {
23180
25172
  return JSON.parse(raw);
23181
25173
  } catch (error) {
@@ -23185,6 +25177,49 @@ function parseJsonFileOrInline8(value, flag) {
23185
25177
  function collectRepeatable5(value, previous) {
23186
25178
  return [...previous, value];
23187
25179
  }
25180
+ var HTTP_URL_PATTERN2 = /^https?:\/\//i;
25181
+ async function uploadLocalFile(client, filePath) {
25182
+ const bytes = await readFile3(filePath);
25183
+ const name = basename2(filePath);
25184
+ const result = await client.uploadMedia({
25185
+ name,
25186
+ mimeType: inferMimeType(name, bytes),
25187
+ data: bytes.toString("base64")
25188
+ });
25189
+ return { ...result, name, size: bytes.length };
25190
+ }
25191
+ async function resolveCoverUrl(client, value) {
25192
+ if (HTTP_URL_PATTERN2.test(value)) return value;
25193
+ const uploaded = await uploadLocalFile(client, value);
25194
+ return uploaded.url;
25195
+ }
25196
+ async function setProductCoverImages(client, productId, urls) {
25197
+ for (const url of urls) assertHttpsUrl(url, "--image");
25198
+ return client.setProductImages(productId, urls);
25199
+ }
25200
+ function printFilesTable(rows) {
25201
+ printTable(
25202
+ rows.map((f) => ({
25203
+ uid: f.uid,
25204
+ name: f.name,
25205
+ url: f.url,
25206
+ mediaId: f.mediaId ?? "",
25207
+ size: f.size ?? ""
25208
+ })),
25209
+ ["uid", "name", "url", "mediaId", "size"]
25210
+ );
25211
+ }
25212
+ async function addProductFiles(client, productId, inputs) {
25213
+ return client.addDownloadFiles(productId, inputs);
25214
+ }
25215
+ function printAddedFiles(inputCount, result) {
25216
+ const skipped = inputCount - result.added.length;
25217
+ process.stdout.write(
25218
+ `Added ${result.added.length} file(s)${skipped > 0 ? ` (${skipped} skipped \u2014 already attached)` : ""}; ${result.files.length} total.
25219
+ `
25220
+ );
25221
+ printFilesTable(result.files);
25222
+ }
23188
25223
  function registerProductsCommands(program2) {
23189
25224
  const products2 = program2.command("products").description("Manage products");
23190
25225
  products2.command("list").description("List products in the workspace").action(async (_, command) => {
@@ -23230,18 +25265,54 @@ function registerProductsCommands(program2) {
23230
25265
  ).requiredOption("--name <name>", "product name").requiredOption("--type <type>", "product type (course, newsletter, community, ...)").option("--description <description>", "product description").option("--free", "seed a free default offer").option("--amount <amount>", "seed a paid default offer \u2014 price in MAJOR units (49.99)").option("--currency <currency>", "currency for --amount (pln, usd, eur, gbp)").option(
23231
25266
  "--period <period>",
23232
25267
  "billing period for --amount: one_time, monthly or yearly (default one_time)"
25268
+ ).option("--description-file <path>", "read the product description from a file").option(
25269
+ "--cover <pathOrUrl>",
25270
+ "cover image: a local file (uploaded to the media library first) or an https URL"
25271
+ ).option(
25272
+ "--file <path>",
25273
+ "local file to upload and attach to the product (downloads products; repeat for several)",
25274
+ collectRepeatable5,
25275
+ []
23233
25276
  ).action(
23234
25277
  async (options, command) => {
23235
25278
  const flags = getGlobalFlags(command);
23236
25279
  const offer = parseCreateOfferFlags(options);
25280
+ if (options.description !== void 0 && options.descriptionFile !== void 0) {
25281
+ throw new Error("Use either --description or --description-file, not both.");
25282
+ }
25283
+ const description = options.descriptionFile !== void 0 ? readFileSync22(options.descriptionFile, "utf8") : options.description;
23237
25284
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23238
25285
  const product = await client.createProduct({
23239
25286
  name: options.name,
23240
25287
  productType: options.type,
23241
- description: options.description,
25288
+ description,
23242
25289
  offer
23243
25290
  });
23244
- emit(flags, product, () => {
25291
+ const outcome = {};
25292
+ try {
25293
+ if (options.cover !== void 0) {
25294
+ const coverUrl = await resolveCoverUrl(client, options.cover);
25295
+ outcome.cover = await setProductCoverImages(client, product.id, [coverUrl]);
25296
+ }
25297
+ if (options.file.length > 0) {
25298
+ const uploaded = [];
25299
+ for (const filePath of options.file)
25300
+ uploaded.push(await uploadLocalFile(client, filePath));
25301
+ outcome.uploadedFiles = uploaded;
25302
+ outcome.files = await addProductFiles(
25303
+ client,
25304
+ product.id,
25305
+ uploaded.map((u) => ({ url: u.url, name: u.name, mediaId: u.mediaId, size: u.size }))
25306
+ );
25307
+ }
25308
+ } catch (error) {
25309
+ process.stderr.write(
25310
+ `Product ${product.id} was created, but a follow-up step failed \u2014 fix it with \`products images set\` / \`products files add\`.
25311
+ `
25312
+ );
25313
+ throw error;
25314
+ }
25315
+ emit(flags, { ...product, ...outcome }, () => {
23245
25316
  process.stdout.write(`Created product ${product.id} \u2014 ${product.name}
23246
25317
  `);
23247
25318
  if (offer) {
@@ -23250,6 +25321,17 @@ function registerProductsCommands(program2) {
23250
25321
  ` : " Default offer was requested but not seeded (check the dashboard).\n"
23251
25322
  );
23252
25323
  }
25324
+ if (outcome.cover) {
25325
+ process.stdout.write(` Cover image: ${outcome.cover.images.join(", ")}
25326
+ `);
25327
+ }
25328
+ if (outcome.uploadedFiles && outcome.files) {
25329
+ process.stdout.write(
25330
+ ` Uploaded ${outcome.uploadedFiles.length} file(s): ${outcome.uploadedFiles.map((u) => `${u.name} (${u.mediaId})`).join(", ")}
25331
+ `
25332
+ );
25333
+ printAddedFiles(outcome.uploadedFiles.length, outcome.files);
25334
+ }
23253
25335
  });
23254
25336
  }
23255
25337
  );
@@ -23727,9 +25809,8 @@ function registerProductsCommands(program2) {
23727
25809
  if (options.image.length === 0) {
23728
25810
  throw new Error("Pass at least one --image <url>.");
23729
25811
  }
23730
- for (const url of options.image) assertHttpsUrl(url, "--image");
23731
25812
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23732
- const result = await client.setProductImages(productId, options.image);
25813
+ const result = await setProductCoverImages(client, productId, options.image);
23733
25814
  emit(flags, result, () => {
23734
25815
  process.stdout.write(
23735
25816
  `Set ${result.images.length} cover image(s) on product ${productId}.
@@ -23738,18 +25819,6 @@ function registerProductsCommands(program2) {
23738
25819
  });
23739
25820
  });
23740
25821
  const files = products2.command("files").description("Manage a downloads product content (Markdown description + file list)");
23741
- const printFilesTable = (rows) => {
23742
- printTable(
23743
- rows.map((f) => ({
23744
- uid: f.uid,
23745
- name: f.name,
23746
- url: f.url,
23747
- mediaId: f.mediaId ?? "",
23748
- size: f.size ?? ""
23749
- })),
23750
- ["uid", "name", "url", "mediaId", "size"]
23751
- );
23752
- };
23753
25822
  files.command("get").description("Show the downloads content: description presence + file table").requiredOption("--product <id>", "downloads product id").action(async (options, command) => {
23754
25823
  const flags = getGlobalFlags(command);
23755
25824
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -23770,7 +25839,7 @@ function registerProductsCommands(program2) {
23770
25839
  if (options.content !== void 0 && options.contentFile !== void 0) {
23771
25840
  throw new Error("Use either --content or --content-file, not both.");
23772
25841
  }
23773
- const editorContent = options.contentFile !== void 0 ? readFileSync20(options.contentFile, "utf8") : options.content;
25842
+ const editorContent = options.contentFile !== void 0 ? readFileSync22(options.contentFile, "utf8") : options.content;
23774
25843
  if (editorContent === void 0) {
23775
25844
  throw new Error("Pass the description via --content or --content-file.");
23776
25845
  }
@@ -23816,14 +25885,9 @@ function registerProductsCommands(program2) {
23816
25885
  ];
23817
25886
  }
23818
25887
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23819
- const result = await client.addDownloadFiles(options.product, inputs);
25888
+ const result = await addProductFiles(client, options.product, inputs);
23820
25889
  emit(flags, result, () => {
23821
- const skipped = inputs.length - result.added.length;
23822
- process.stdout.write(
23823
- `Added ${result.added.length} file(s)${skipped > 0 ? ` (${skipped} skipped \u2014 already attached)` : ""}; ${result.files.length} total.
23824
- `
23825
- );
23826
- printFilesTable(result.files);
25890
+ printAddedFiles(inputs.length, result);
23827
25891
  });
23828
25892
  }
23829
25893
  );
@@ -23869,7 +25933,7 @@ function registerProductsCommands(program2) {
23869
25933
  if (options.content !== void 0 && options.contentFile !== void 0) {
23870
25934
  throw new Error("Use either --content or --content-file, not both.");
23871
25935
  }
23872
- return options.contentFile !== void 0 ? readFileSync20(options.contentFile, "utf8") : options.content;
25936
+ return options.contentFile !== void 0 ? readFileSync22(options.contentFile, "utf8") : options.content;
23873
25937
  };
23874
25938
  kb.command("get").description(
23875
25939
  "Show the knowledge base: categories (tree keys = the --category ids) + all articles (drafts and unavailable included \u2014 the creator view)"
@@ -24089,7 +26153,7 @@ function registerProductsCommands(program2) {
24089
26153
  }
24090
26154
 
24091
26155
  // src/commands/referrals/index.ts
24092
- import { readFileSync as readFileSync21 } from "fs";
26156
+ import { readFileSync as readFileSync23 } from "fs";
24093
26157
  var PROGRAM_KINDS = ["referral", "affiliate"];
24094
26158
  var WRITABLE_STATUSES = ["active", "inactive"];
24095
26159
  var ENROLLMENT_MODES = ["open", "inviteOnly", "closed"];
@@ -24114,7 +26178,7 @@ function parseIdList3(value, flag) {
24114
26178
  }
24115
26179
  function parseJsonObjectFileOrInline4(value, flag) {
24116
26180
  const trimmed = value.trim();
24117
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync21(value, "utf8");
26181
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync23(value, "utf8");
24118
26182
  let parsed;
24119
26183
  try {
24120
26184
  parsed = JSON.parse(raw);
@@ -24152,7 +26216,7 @@ function parseDeepConfig(value) {
24152
26216
  }
24153
26217
  return parsed;
24154
26218
  }
24155
- function requireConfirm10(confirm, what) {
26219
+ function requireConfirm13(confirm, what) {
24156
26220
  if (!confirm) {
24157
26221
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
24158
26222
  }
@@ -24387,7 +26451,7 @@ Failed deliveries (${result.failedDeliveries.length})
24387
26451
  "Archive a program \u2014 new referrals stop counting and it leaves the active list. Requires --confirm."
24388
26452
  ).argument("<programId>", "program id").option("--confirm", "required \u2014 acknowledge the archive").action(async (programId, options, command) => {
24389
26453
  const flags = getGlobalFlags(command);
24390
- requireConfirm10(options.confirm, `archive program ${programId}`);
26454
+ requireConfirm13(options.confirm, `archive program ${programId}`);
24391
26455
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24392
26456
  const result = await client.archiveReferralProgram(programId);
24393
26457
  emit(flags, result, () => {
@@ -24419,7 +26483,7 @@ Failed deliveries (${result.failedDeliveries.length})
24419
26483
  "Enroll EVERY client with access to one of the program products. Requires --confirm. Reports enrolled / alreadyEnrolled / skippedWithoutAccount counts."
24420
26484
  ).argument("<programId>", "program id").option("--confirm", "required \u2014 acknowledge the bulk enrollment").action(async (programId, options, command) => {
24421
26485
  const flags = getGlobalFlags(command);
24422
- requireConfirm10(options.confirm, `enroll all clients in program ${programId}`);
26486
+ requireConfirm13(options.confirm, `enroll all clients in program ${programId}`);
24423
26487
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24424
26488
  const result = await client.enrollAllReferralClients(programId);
24425
26489
  emit(flags, result, () => {
@@ -24435,7 +26499,7 @@ Failed deliveries (${result.failedDeliveries.length})
24435
26499
  async (programId, entryId, options, command) => {
24436
26500
  const flags = getGlobalFlags(command);
24437
26501
  const decision = parseChoice6(options.decision, REWARD_DECISIONS, "--decision");
24438
- requireConfirm10(options.confirm, `${decision} reward ${entryId} in program ${programId}`);
26502
+ requireConfirm13(options.confirm, `${decision} reward ${entryId} in program ${programId}`);
24439
26503
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24440
26504
  const result = await client.resolveReferralReward(programId, entryId, { decision });
24441
26505
  emit(flags, result, () => {
@@ -24465,7 +26529,7 @@ Failed deliveries (${result.failedDeliveries.length})
24465
26529
  }
24466
26530
 
24467
26531
  // src/commands/segments/index.ts
24468
- import { readFileSync as readFileSync22 } from "fs";
26532
+ import { readFileSync as readFileSync24 } from "fs";
24469
26533
  var SUBSCRIPTION_STATUSES2 = ["pending", "subscribed", "unsubscribed"];
24470
26534
  function collectRepeatable6(value, previous) {
24471
26535
  return [...previous, value];
@@ -24477,14 +26541,14 @@ function parseMinorUnits(value, flag) {
24477
26541
  }
24478
26542
  return n;
24479
26543
  }
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";
26544
+ var CONDITION_FLAGS_HELP = "--has-tag/--without-tag, --has-any-tag/--has-none-tag, --owns/--not-owns, --owns-direct/--not-owns-direct, --spent-gte/--spent-lte, --subscription, or --json-file";
24481
26545
  function resolveConditions(options) {
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;
26546
+ const hasFlagConditions = options.hasTag.length > 0 || options.withoutTag.length > 0 || options.hasAnyTag.length > 0 || options.hasNoneTag.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;
24483
26547
  if (options.jsonFile !== void 0) {
24484
26548
  if (hasFlagConditions) {
24485
26549
  throw new Error("Use either condition flags or --json-file, not both.");
24486
26550
  }
24487
- const raw = readFileSync22(options.jsonFile, "utf8");
26551
+ const raw = readFileSync24(options.jsonFile, "utf8");
24488
26552
  let parsed;
24489
26553
  try {
24490
26554
  parsed = JSON.parse(raw);
@@ -24506,6 +26570,12 @@ function resolveConditions(options) {
24506
26570
  for (const tagId of options.withoutTag) {
24507
26571
  conditions.push({ field: "tag", operator: "not-has", tagId });
24508
26572
  }
26573
+ if (options.hasAnyTag.length > 0) {
26574
+ conditions.push({ field: "tag", operator: "has-any", tagIds: options.hasAnyTag });
26575
+ }
26576
+ if (options.hasNoneTag.length > 0) {
26577
+ conditions.push({ field: "tag", operator: "has-none", tagIds: options.hasNoneTag });
26578
+ }
24509
26579
  for (const productId of options.owns) {
24510
26580
  conditions.push({ field: "product", operator: "owns", productId });
24511
26581
  }
@@ -24541,6 +26611,12 @@ function resolveConditions(options) {
24541
26611
  }
24542
26612
  conditions.push({ field: "emailSubscription", operator: "is", status });
24543
26613
  }
26614
+ if (options.notOpened !== void 0) {
26615
+ conditions.push({ field: "campaign", operator: "not-opened", campaignId: options.notOpened });
26616
+ }
26617
+ if (options.notClicked !== void 0) {
26618
+ conditions.push({ field: "campaign", operator: "not-clicked", campaignId: options.notClicked });
26619
+ }
24544
26620
  return conditions;
24545
26621
  }
24546
26622
  function withConditionOptions(command) {
@@ -24554,6 +26630,16 @@ function withConditionOptions(command) {
24554
26630
  "client does NOT carry this tag (repeatable)",
24555
26631
  collectRepeatable6,
24556
26632
  []
26633
+ ).option(
26634
+ "--has-any-tag <tagId>",
26635
+ "client carries AT LEAST ONE of these tags (repeat the flag; one OR condition, max 10 ids) - one send to several tags without double-mailing",
26636
+ collectRepeatable6,
26637
+ []
26638
+ ).option(
26639
+ "--has-none-tag <tagId>",
26640
+ "client carries NONE of these tags (repeat the flag; one condition, max 10 ids)",
26641
+ collectRepeatable6,
26642
+ []
24557
26643
  ).option(
24558
26644
  "--owns <productId>",
24559
26645
  "client has ACTIVE access to this product - a direct grant OR through a bundle (repeatable)",
@@ -24580,6 +26666,12 @@ function withConditionOptions(command) {
24580
26666
  ).option("--spent-lte <minorUnits>", "lifetime spend at most this (minor units)").option(
24581
26667
  "--subscription <status>",
24582
26668
  `marketing-email subscription state is (${SUBSCRIPTION_STATUSES2.join(" | ")})`
26669
+ ).option(
26670
+ "--not-opened <campaignId>",
26671
+ "received this SENT campaign and never opened it (the resend-to-non-openers audience)"
26672
+ ).option(
26673
+ "--not-clicked <campaignId>",
26674
+ "received this SENT campaign and never clicked a link in it"
24583
26675
  ).option(
24584
26676
  "--json-file <path>",
24585
26677
  "read the conditions as a JSON array from a file instead of flags"
@@ -24588,6 +26680,9 @@ function withConditionOptions(command) {
24588
26680
  function describeCondition(condition) {
24589
26681
  switch (condition.field) {
24590
26682
  case "tag":
26683
+ if ("tagIds" in condition) {
26684
+ return `${condition.operator === "has-any" ? "has any of" : "has none of"} tags ${condition.tagIds.join(", ")}`;
26685
+ }
24591
26686
  return `${condition.operator === "has" ? "has" : "lacks"} tag ${condition.tagId}`;
24592
26687
  case "product":
24593
26688
  return `${condition.operator === "owns" ? "owns" : "does not own"} product ${condition.productId}${condition.accessScope === "direct" ? " (direct)" : ""}`;
@@ -24603,6 +26698,8 @@ function describeCondition(condition) {
24603
26698
  return `sms consent is ${condition.status}`;
24604
26699
  case "phoneNumber":
24605
26700
  return condition.operator === "has" ? "has a phone number" : "has no phone number";
26701
+ case "campaign":
26702
+ return `${condition.operator.replace("-", " ")} campaign ${condition.campaignId}`;
24606
26703
  }
24607
26704
  }
24608
26705
  function registerSegmentsCommands(program2) {
@@ -24700,7 +26797,7 @@ function registerSegmentsCommands(program2) {
24700
26797
  }
24701
26798
 
24702
26799
  // src/commands/site/index.ts
24703
- import { existsSync as existsSync4, readFileSync as readFileSync23 } from "fs";
26800
+ import { existsSync as existsSync4, readFileSync as readFileSync25 } from "fs";
24704
26801
  var MAIN_PAGE_MODES = {
24705
26802
  sales: "/",
24706
26803
  dashboard: "/dashboard",
@@ -24724,14 +26821,14 @@ var HTTP_CONFLICT = 409;
24724
26821
  var HTTP_BAD_REQUEST = 400;
24725
26822
  var HTTP_NOT_FOUND = 404;
24726
26823
  var HTTP_FORBIDDEN = 403;
24727
- function requireConfirm11(confirm, what) {
26824
+ function requireConfirm14(confirm, what) {
24728
26825
  if (!confirm) {
24729
26826
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
24730
26827
  }
24731
26828
  }
24732
26829
  function parseJsonObjectFileOrInline5(value, flag) {
24733
26830
  const trimmed = value.trim();
24734
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync23(value, "utf8");
26831
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync25(value, "utf8");
24735
26832
  let parsed;
24736
26833
  try {
24737
26834
  parsed = JSON.parse(raw);
@@ -24746,7 +26843,7 @@ function parseJsonObjectFileOrInline5(value, flag) {
24746
26843
  function baseBody(json) {
24747
26844
  return json === void 0 ? {} : parseJsonObjectFileOrInline5(json, "--json");
24748
26845
  }
24749
- function parseOnOff2(value, flag) {
26846
+ function parseOnOff4(value, flag) {
24750
26847
  const v = value.trim().toLowerCase();
24751
26848
  if (v === "on" || v === "true") return true;
24752
26849
  if (v === "off" || v === "false") return false;
@@ -24783,7 +26880,7 @@ function parseDomainList(value, what) {
24783
26880
  return items;
24784
26881
  }
24785
26882
  function readMarkdownOrFile(value) {
24786
- return existsSync4(value) ? readFileSync23(value, "utf8") : value;
26883
+ return existsSync4(value) ? readFileSync25(value, "utf8") : value;
24787
26884
  }
24788
26885
  function assertNonEmptyBody(body, hint) {
24789
26886
  if (Object.keys(body).length === 0) {
@@ -24990,7 +27087,7 @@ function registerSiteCommands(program2) {
24990
27087
  body.language = parseChoice7(options.language, PLATFORM_LANGUAGES, "--language");
24991
27088
  }
24992
27089
  if (options.showPageTitle !== void 0) {
24993
- body.showPageTitle = parseOnOff2(options.showPageTitle, "--show-page-title");
27090
+ body.showPageTitle = parseOnOff4(options.showPageTitle, "--show-page-title");
24994
27091
  }
24995
27092
  assertNonEmptyBody(body, "pass --title, --description, --language or --json=<patch>.");
24996
27093
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -25012,7 +27109,7 @@ function registerSiteCommands(program2) {
25012
27109
  if (options.radius !== void 0) body.radius = options.radius;
25013
27110
  if (options.styleVariant !== void 0) body.styleVariant = options.styleVariant;
25014
27111
  if (options.fromBrand !== void 0) {
25015
- body.themeFromBrand = parseOnOff2(options.fromBrand, "--from-brand");
27112
+ body.themeFromBrand = parseOnOff4(options.fromBrand, "--from-brand");
25016
27113
  }
25017
27114
  assertNonEmptyBody(body, "pass --theme, --radius, --from-brand \u2026 or --json=<patch>.");
25018
27115
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -25039,7 +27136,7 @@ function registerSiteCommands(program2) {
25039
27136
  body.platformFaviconUrl = orNull6(options.faviconUrl);
25040
27137
  }
25041
27138
  if (options.hideBranding !== void 0) {
25042
- body.hideZanfiaBranding = parseOnOff2(options.hideBranding, "--hide-branding");
27139
+ body.hideZanfiaBranding = parseOnOff4(options.hideBranding, "--hide-branding");
25043
27140
  }
25044
27141
  assertNonEmptyBody(
25045
27142
  body,
@@ -25084,7 +27181,7 @@ function registerSiteCommands(program2) {
25084
27181
  update.command("privacy").description("Cookie-consent banner on the public site").option("--cookie-consent <on|off>", "show the cookie consent banner").option("--json <fileOrJson>", "inline JSON object or .json path \u2014 cookieConsentEnabled").action(async (options, command) => {
25085
27182
  const flags = getGlobalFlags(command);
25086
27183
  const base = baseBody(options.json);
25087
- const cookieConsentEnabled = options.cookieConsent !== void 0 ? parseOnOff2(options.cookieConsent, "--cookie-consent") : base.cookieConsentEnabled;
27184
+ const cookieConsentEnabled = options.cookieConsent !== void 0 ? parseOnOff4(options.cookieConsent, "--cookie-consent") : base.cookieConsentEnabled;
25088
27185
  if (typeof cookieConsentEnabled !== "boolean") {
25089
27186
  throw new Error(
25090
27187
  'Pass --cookie-consent on|off (or a --json body with "cookieConsentEnabled").'
@@ -25151,14 +27248,14 @@ function registerSiteCommands(program2) {
25151
27248
  if (options.libraryOrder === void 0) customization.libraryOrder = "custom";
25152
27249
  }
25153
27250
  if (options.groupByType !== void 0) {
25154
- customization.libraryGroupByType = parseOnOff2(options.groupByType, "--group-by-type");
27251
+ customization.libraryGroupByType = parseOnOff4(options.groupByType, "--group-by-type");
25155
27252
  }
25156
27253
  if (options.defaultView !== void 0) customization.defaultView = options.defaultView;
25157
27254
  if (options.messages !== void 0) {
25158
- customization.messagesEnabled = parseOnOff2(options.messages, "--messages");
27255
+ customization.messagesEnabled = parseOnOff4(options.messages, "--messages");
25159
27256
  }
25160
27257
  if (options.clientMessages !== void 0) {
25161
- customization.clientMessagesEnabled = parseOnOff2(
27258
+ customization.clientMessagesEnabled = parseOnOff4(
25162
27259
  options.clientMessages,
25163
27260
  "--client-messages"
25164
27261
  );
@@ -25266,7 +27363,7 @@ function registerSiteCommands(program2) {
25266
27363
  throw new Error("Pass exactly one of --on or --off.");
25267
27364
  }
25268
27365
  const active = Boolean(options.on);
25269
- requireConfirm11(options.confirm, active ? "publish the site" : "take the site offline");
27366
+ requireConfirm14(options.confirm, active ? "publish the site" : "take the site offline");
25270
27367
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25271
27368
  const result = await client.activateSite({ active });
25272
27369
  emit(
@@ -25281,7 +27378,7 @@ function registerSiteCommands(program2) {
25281
27378
  "Change the slug \u2014 heavy: re-registers Stripe/Firebase/Vercel hosts and purges caches. 409 = slug taken. Requires --confirm"
25282
27379
  ).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) => {
25283
27380
  const flags = getGlobalFlags(command);
25284
- requireConfirm11(options.confirm, `change the site slug to "${value}"`);
27381
+ requireConfirm14(options.confirm, `change the site slug to "${value}"`);
25285
27382
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25286
27383
  const result = await client.updateSiteSlug({ slug: value.trim() }).catch(withSlugHint);
25287
27384
  emit(flags, result, () => process.stdout.write(`Slug changed to "${value.trim()}".
@@ -25293,7 +27390,7 @@ function registerSiteCommands(program2) {
25293
27390
  ).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) => {
25294
27391
  const flags = getGlobalFlags(command);
25295
27392
  const list = value.trim().toLowerCase() === "none" ? [] : parseDomainList(value, "<domains>");
25296
- requireConfirm11(
27393
+ requireConfirm14(
25297
27394
  options.confirm,
25298
27395
  list.length === 0 ? "remove every custom domain" : `set the custom domains to ${list.join(", ")}`
25299
27396
  );
@@ -25378,7 +27475,7 @@ function registerSiteCommands(program2) {
25378
27475
  "Nothing to update \u2014 pass --terms-content|--terms-url and/or --privacy-content|--privacy-url."
25379
27476
  );
25380
27477
  }
25381
- requireConfirm11(options.confirm, "change the legal documents");
27478
+ requireConfirm14(options.confirm, "change the legal documents");
25382
27479
  const body = {};
25383
27480
  if (options.language !== void 0) body.language = options.language;
25384
27481
  if (terms) body.terms = terms;
@@ -25430,8 +27527,8 @@ function printStorefront(config) {
25430
27527
  }
25431
27528
 
25432
27529
  // src/commands/smsCampaigns/index.ts
25433
- import { readFileSync as readFileSync24 } from "fs";
25434
- var SMS_PROVIDERS = ["twilio", "smsapi"];
27530
+ import { readFileSync as readFileSync26 } from "fs";
27531
+ var SMS_PROVIDERS2 = ["twilio", "smsapi"];
25435
27532
  var RECIPIENT_STATUSES2 = [
25436
27533
  "pending",
25437
27534
  "sent",
@@ -25446,7 +27543,7 @@ function parsePositiveInt15(value, flag) {
25446
27543
  }
25447
27544
  return n;
25448
27545
  }
25449
- function requireConfirm12(confirm, refusal) {
27546
+ function requireConfirm15(confirm, refusal) {
25450
27547
  if (!confirm) throw new Error(refusal);
25451
27548
  }
25452
27549
  function withSchedulerHint4(error) {
@@ -25463,7 +27560,7 @@ function resolveBody(options) {
25463
27560
  if (options.body !== void 0 && options.bodyFile !== void 0) {
25464
27561
  throw new Error("Use either --body or --body-file, not both.");
25465
27562
  }
25466
- return options.bodyFile !== void 0 ? readFileSync24(options.bodyFile, "utf8") : options.body;
27563
+ return options.bodyFile !== void 0 ? readFileSync26(options.bodyFile, "utf8") : options.body;
25467
27564
  }
25468
27565
  function resolveAudience2(options) {
25469
27566
  if (options.segment !== void 0 && options.allSubscribed) {
@@ -25482,10 +27579,10 @@ function resolveProvider(options) {
25482
27579
  }
25483
27580
  if (options.defaultProvider) return null;
25484
27581
  if (options.provider === void 0) return void 0;
25485
- const match = SMS_PROVIDERS.find((p) => p === options.provider);
27582
+ const match = SMS_PROVIDERS2.find((p) => p === options.provider);
25486
27583
  if (!match) {
25487
27584
  throw new Error(
25488
- `Invalid --provider: ${options.provider} (expected one of ${SMS_PROVIDERS.join(", ")})`
27585
+ `Invalid --provider: ${options.provider} (expected one of ${SMS_PROVIDERS2.join(", ")})`
25489
27586
  );
25490
27587
  }
25491
27588
  return match;
@@ -25570,7 +27667,7 @@ function registerSmsCampaignsCommands(program2) {
25570
27667
  });
25571
27668
  smsCampaigns.command("create").description(
25572
27669
  "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(
27670
+ ).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_PROVIDERS2.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
27671
  async (options, command) => {
25575
27672
  const flags = getGlobalFlags(command);
25576
27673
  const body = resolveBody(options);
@@ -25593,7 +27690,7 @@ function registerSmsCampaignsCommands(program2) {
25593
27690
  });
25594
27691
  }
25595
27692
  );
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(
27693
+ 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_PROVIDERS2.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
27694
  async (id, options, command) => {
25598
27695
  const flags = getGlobalFlags(command);
25599
27696
  const input = {};
@@ -25625,7 +27722,7 @@ function registerSmsCampaignsCommands(program2) {
25625
27722
  "PERMANENTLY delete an SMS campaign \u2014 cannot be undone; draft/cancelled/failed only (sent campaigns are audit records and stay). Requires --confirm"
25626
27723
  ).argument("<id>", "SMS campaign id").option("--confirm", "required \u2014 acknowledge the permanent deletion").action(async (id, options, command) => {
25627
27724
  const flags = getGlobalFlags(command);
25628
- requireConfirm12(
27725
+ requireConfirm15(
25629
27726
  options.confirm,
25630
27727
  `Refusing to delete SMS campaign ${id} without --confirm. Re-run with --confirm to proceed.`
25631
27728
  );
@@ -25643,7 +27740,7 @@ function registerSmsCampaignsCommands(program2) {
25643
27740
  "schedule the send (ISO, max 30 days out) instead of sending now"
25644
27741
  ).option("--confirm", "required \u2014 acknowledge this texts the audience for real").action(async (id, options, command) => {
25645
27742
  const flags = getGlobalFlags(command);
25646
- requireConfirm12(
27743
+ requireConfirm15(
25647
27744
  options.confirm,
25648
27745
  `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
27746
  );
@@ -26272,7 +28369,7 @@ function describeAttempts(item) {
26272
28369
  const next = item.nextPaymentAttempt !== null ? formatEpochSeconds(item.nextPaymentAttempt) : "";
26273
28370
  return next ? `${attempts} (next ${next})` : String(attempts);
26274
28371
  }
26275
- function requireConfirm13(confirm, what) {
28372
+ function requireConfirm16(confirm, what) {
26276
28373
  if (!confirm) {
26277
28374
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
26278
28375
  }
@@ -26346,7 +28443,7 @@ function registerSubscriptionsCommands(program2) {
26346
28443
  "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"
26347
28444
  ).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) => {
26348
28445
  const flags = getGlobalFlags(command);
26349
- requireConfirm13(options.confirm, `retry the charge for invoice ${invoiceId} (real charge)`);
28446
+ requireConfirm16(options.confirm, `retry the charge for invoice ${invoiceId} (real charge)`);
26350
28447
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26351
28448
  const result = await client.retrySubscriptionPayment(invoiceId);
26352
28449
  emit(flags, result, () => {
@@ -26883,7 +28980,7 @@ Next page: --cursor ${page.nextCursor}
26883
28980
  }
26884
28981
 
26885
28982
  // src/commands/workflows/index.ts
26886
- import { readFileSync as readFileSync25 } from "fs";
28983
+ import { readFileSync as readFileSync27 } from "fs";
26887
28984
 
26888
28985
  // src/commands/workflows/authoring.ts
26889
28986
  import { writeFileSync as writeFileSync6 } from "fs";
@@ -26911,6 +29008,10 @@ function registerWorkflowAuthoringCommands(workflows) {
26911
29008
  ).option(
26912
29009
  "--trigger <eventType>",
26913
29010
  "business event that starts it, e.g. product.ProductPurchased (see `workflows list` for examples)"
29011
+ ).option(
29012
+ "--kind <kind>",
29013
+ "transactional (default for purchase/access triggers: no unsubscribe link, reaches everyone) or marketing (unsubscribe link + headers, e-mail/SMS steps skipped for unsubscribed contacts); omitted = derived from the triggers at publish",
29014
+ parseWorkflowKind
26914
29015
  ).action(
26915
29016
  async (options, command) => {
26916
29017
  const flags = getGlobalFlags(command);
@@ -26918,6 +29019,7 @@ function registerWorkflowAuthoringCommands(workflows) {
26918
29019
  if (options.description !== void 0) input.description = options.description;
26919
29020
  if (options.product !== void 0) input.productIds = options.product;
26920
29021
  if (options.trigger !== void 0) input.triggerEventType = options.trigger;
29022
+ if (options.kind !== void 0) input.kind = options.kind;
26921
29023
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26922
29024
  const result = await client.createAutomation(input);
26923
29025
  emit(flags, result, () => {
@@ -26972,6 +29074,10 @@ function registerWorkflowAuthoringCommands(workflows) {
26972
29074
  "--product <ids>",
26973
29075
  "also replace the product scope (comma-separated ids)",
26974
29076
  parseCommaList("--product")
29077
+ ).option(
29078
+ "--kind <kind>",
29079
+ "also set the kind: transactional or marketing (see `workflows create --help`)",
29080
+ parseWorkflowKind
26975
29081
  ).action(
26976
29082
  async (automationId, options, command) => {
26977
29083
  const flags = getGlobalFlags(command);
@@ -26980,7 +29086,8 @@ function registerWorkflowAuthoringCommands(workflows) {
26980
29086
  draft: options.json,
26981
29087
  ...options.name !== void 0 ? { name: options.name } : {},
26982
29088
  ...options.description !== void 0 ? { description: options.description } : {},
26983
- ...options.product !== void 0 ? { productIds: options.product } : {}
29089
+ ...options.product !== void 0 ? { productIds: options.product } : {},
29090
+ ...options.kind !== void 0 ? { kind: options.kind } : {}
26984
29091
  }).catch(withGraphHint);
26985
29092
  emit(flags, result, () => {
26986
29093
  if (result.issues.length > 0) {
@@ -27049,14 +29156,14 @@ function parsePositiveInt18(value, flag) {
27049
29156
  }
27050
29157
  return n;
27051
29158
  }
27052
- function requireConfirm14(confirm, what) {
29159
+ function requireConfirm17(confirm, what) {
27053
29160
  if (!confirm) {
27054
29161
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
27055
29162
  }
27056
29163
  }
27057
29164
  function parseTriggerContext(value) {
27058
29165
  const trimmed = value.trim();
27059
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync25(value, "utf8");
29166
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync27(value, "utf8");
27060
29167
  let parsed;
27061
29168
  try {
27062
29169
  parsed = JSON.parse(raw);
@@ -27093,51 +29200,36 @@ function registerWorkflowsCommands(program2) {
27093
29200
  id: w.id,
27094
29201
  name: w.name,
27095
29202
  status: w.status,
29203
+ kind: w.kind,
27096
29204
  triggers: w.triggers.join(", "),
27097
29205
  products: formatProductScope(w),
27098
29206
  runsThisMonth: w.runsThisMonth,
27099
29207
  lastRun: w.lastRunAt ?? ""
27100
29208
  })),
27101
- ["id", "name", "status", "triggers", "products", "runsThisMonth", "lastRun"]
29209
+ ["id", "name", "status", "kind", "triggers", "products", "runsThisMonth", "lastRun"]
27102
29210
  );
27103
29211
  });
27104
29212
  });
27105
- workflows.command("get").description("Show a single workflow (triggers, product scope, node counts, last run)").argument("<automationId>", "workflow automation id").action(async (automationId, _, command) => {
29213
+ workflows.command("get").description(
29214
+ "Show a single workflow (triggers, product scope, node counts, last run); --graph walks every step incl. the emails it sends"
29215
+ ).argument("<automationId>", "workflow automation id").option(
29216
+ "--graph",
29217
+ "also print the graph step by step in execution order \u2014 email subjects and body excerpts, delays, condition branches. Shows the published graph, plus the draft when it differs. With -o json the raw response includes draftGraph/publishedGraph."
29218
+ ).action(async (automationId, options, command) => {
27106
29219
  const flags = getGlobalFlags(command);
27107
29220
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27108
- const workflow = await client.getAutomation(automationId);
29221
+ if (!options.graph) {
29222
+ const workflow2 = await client.getAutomation(automationId);
29223
+ emit(flags, workflow2, () => printWorkflowSummary(workflow2));
29224
+ return;
29225
+ }
29226
+ const workflow = await client.getAutomationGraph(automationId);
29227
+ const published = asAutomationGraph(workflow.publishedGraph);
29228
+ const draft = asAutomationGraph(workflow.draftGraph);
29229
+ const templateNames = await resolveTemplateNames(client, [published, draft]);
27109
29230
  emit(flags, workflow, () => {
27110
- process.stdout.write(`Workflow ${workflow.name} (${workflow.id})
27111
- `);
27112
- if (workflow.description) process.stdout.write(` Description: ${workflow.description}
27113
- `);
27114
- process.stdout.write(` Status: ${workflow.status}
27115
- `);
27116
- process.stdout.write(` Triggers: ${workflow.triggers.join(", ")}
27117
- `);
27118
- process.stdout.write(
27119
- ` Products: ${workflow.productIds.length > 0 ? workflow.productIds.join(", ") : "all"}
27120
- `
27121
- );
27122
- process.stdout.write(
27123
- ` Published: ${workflow.published ? `yes${workflow.publishedAt ? ` (${workflow.publishedAt})` : ""}` : "no (draft \u2014 cannot run until published from the dashboard)"}
27124
- `
27125
- );
27126
- const { nodeCounts } = workflow;
27127
- process.stdout.write(
27128
- ` Nodes: ${nodeCounts.triggers} triggers, ${nodeCounts.actions} actions, ${nodeCounts.conditions} conditions, ${nodeCounts.delays} delays
27129
- `
27130
- );
27131
- process.stdout.write(` Runs this month: ${workflow.runsThisMonth}
27132
- `);
27133
- if (workflow.lastRunAt) {
27134
- process.stdout.write(
27135
- ` Last run: ${workflow.lastRunAt}${workflow.lastRunStatus ? ` (${workflow.lastRunStatus})` : ""}
27136
- `
27137
- );
27138
- }
27139
- if (workflow.createdAt) process.stdout.write(` Created: ${workflow.createdAt}
27140
- `);
29231
+ printWorkflowSummary(workflow);
29232
+ printWorkflowGraphs(workflow, published, draft, templateNames);
27141
29233
  });
27142
29234
  });
27143
29235
  workflows.command("runs").description("List recent runs of a workflow (newest first)").argument("<automationId>", "workflow automation id").option("--limit <n>", "number of runs to return (default 20, max 100)").action(async (automationId, options, command) => {
@@ -27162,13 +29254,17 @@ function registerWorkflowsCommands(program2) {
27162
29254
  );
27163
29255
  });
27164
29256
  });
27165
- workflows.command("pause").description("Pause a workflow \u2014 it stops reacting to its trigger events").argument("<automationId>", "workflow automation id").action(async (automationId, _, command) => {
29257
+ workflows.command("pause").description(
29258
+ "Pause a workflow \u2014 it stops reacting to its trigger events AND cancels runs that are in progress or waiting in a delay (those contacts get no further steps)"
29259
+ ).argument("<automationId>", "workflow automation id").action(async (automationId, _, command) => {
27166
29260
  const flags = getGlobalFlags(command);
27167
29261
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27168
29262
  const result = await client.setAutomationStatus(automationId, "paused");
27169
29263
  emit(flags, result, () => {
27170
- process.stdout.write(`Paused workflow ${result.id}.
27171
- `);
29264
+ process.stdout.write(
29265
+ `Paused workflow ${result.id} (${result.cancelledRuns} in-flight run(s) cancelled).
29266
+ `
29267
+ );
27172
29268
  });
27173
29269
  });
27174
29270
  workflows.command("resume").description(
@@ -27187,7 +29283,7 @@ function registerWorkflowsCommands(program2) {
27187
29283
  ).argument("<automationId>", "workflow automation id").argument("<runId>", "run id (from `workflows runs`)").option("--confirm", "required \u2014 acknowledge that the workflow actions execute again").action(
27188
29284
  async (automationId, runId, options, command) => {
27189
29285
  const flags = getGlobalFlags(command);
27190
- requireConfirm14(options.confirm, `retry run ${runId} (its actions execute again)`);
29286
+ requireConfirm17(options.confirm, `retry run ${runId} (its actions execute again)`);
27191
29287
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27192
29288
  const result = await client.retryAutomationRun(automationId, runId).catch(withSchedulerHint5);
27193
29289
  emit(flags, result, () => {
@@ -27211,7 +29307,7 @@ Follow it with: zanfia workflows runs ${automationId}
27211
29307
  async (automationId, options, command) => {
27212
29308
  const flags = getGlobalFlags(command);
27213
29309
  const context = parseTriggerContext(options.context);
27214
- requireConfirm14(options.confirm, `test-run workflow ${automationId} (real actions fire)`);
29310
+ requireConfirm17(options.confirm, `test-run workflow ${automationId} (real actions fire)`);
27215
29311
  process.stderr.write(
27216
29312
  "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"
27217
29313
  );
@@ -27240,9 +29336,361 @@ function formatSteps(steps) {
27240
29336
  if (steps.skipped > 0) parts.push(`${steps.skipped} skipped`);
27241
29337
  return parts.join(", ");
27242
29338
  }
29339
+ function printWorkflowSummary(workflow) {
29340
+ process.stdout.write(`Workflow ${workflow.name} (${workflow.id})
29341
+ `);
29342
+ if (workflow.description) process.stdout.write(` Description: ${workflow.description}
29343
+ `);
29344
+ process.stdout.write(` Status: ${workflow.status}
29345
+ `);
29346
+ process.stdout.write(` Kind: ${workflow.kind}
29347
+ `);
29348
+ process.stdout.write(` Triggers: ${workflow.triggers.join(", ")}
29349
+ `);
29350
+ process.stdout.write(
29351
+ ` Products: ${workflow.productIds.length > 0 ? workflow.productIds.join(", ") : "all"}
29352
+ `
29353
+ );
29354
+ process.stdout.write(
29355
+ ` Published: ${workflow.published ? `yes${workflow.publishedAt ? ` (${workflow.publishedAt})` : ""}` : "no (draft \u2014 cannot run until published from the dashboard)"}
29356
+ `
29357
+ );
29358
+ const { nodeCounts } = workflow;
29359
+ process.stdout.write(
29360
+ ` Nodes: ${nodeCounts.triggers} triggers, ${nodeCounts.actions} actions, ${nodeCounts.conditions} conditions, ${nodeCounts.delays} delays
29361
+ `
29362
+ );
29363
+ process.stdout.write(` Runs this month: ${workflow.runsThisMonth}
29364
+ `);
29365
+ if (workflow.lastRunAt) {
29366
+ process.stdout.write(
29367
+ ` Last run: ${workflow.lastRunAt}${workflow.lastRunStatus ? ` (${workflow.lastRunStatus})` : ""}
29368
+ `
29369
+ );
29370
+ }
29371
+ if (workflow.createdAt) process.stdout.write(` Created: ${workflow.createdAt}
29372
+ `);
29373
+ }
29374
+ var GRAPH_ACTION_LABELS = {
29375
+ sendEmail: "Send email",
29376
+ notifyOwner: "Notify owner (email)",
29377
+ sendSms: "Send SMS",
29378
+ webhook: "Webhook",
29379
+ publishSocialPost: "Publish social post",
29380
+ createCommunityPost: "Create community post",
29381
+ updateClientTags: "Update client tags",
29382
+ setEmailSubscription: "Set email subscription",
29383
+ openClientDeadline: "Open deadline window",
29384
+ requestTestimonial: "Request testimonial"
29385
+ };
29386
+ var GRAPH_NODE_KINDS = /* @__PURE__ */ new Set([
29387
+ "trigger",
29388
+ "action",
29389
+ "condition",
29390
+ "delay"
29391
+ ]);
29392
+ var GRAPH_CONDITION_BRANCHES = ["true", "false"];
29393
+ var EXCERPT_LENGTH = 120;
29394
+ function isRecord2(value) {
29395
+ return value !== null && typeof value === "object" && !Array.isArray(value);
29396
+ }
29397
+ function optionalString(value) {
29398
+ return typeof value === "string" ? value : void 0;
29399
+ }
29400
+ function asAutomationGraph(raw) {
29401
+ if (!isRecord2(raw) || !Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) return null;
29402
+ const nodes = [];
29403
+ for (const candidate of raw.nodes) {
29404
+ if (!isRecord2(candidate)) continue;
29405
+ const { id, kind, config, name, disabled } = candidate;
29406
+ if (typeof id !== "string" || typeof kind !== "string" || !GRAPH_NODE_KINDS.has(kind)) {
29407
+ continue;
29408
+ }
29409
+ nodes.push({
29410
+ id,
29411
+ kind,
29412
+ config: isRecord2(config) ? config : {},
29413
+ ...typeof name === "string" && name.length > 0 ? { name } : {},
29414
+ ...disabled === true ? { disabled: true } : {}
29415
+ });
29416
+ }
29417
+ const edges = [];
29418
+ for (const candidate of raw.edges) {
29419
+ if (!isRecord2(candidate)) continue;
29420
+ const { source, target, sourceHandle } = candidate;
29421
+ if (typeof source !== "string" || typeof target !== "string") continue;
29422
+ const branch = GRAPH_CONDITION_BRANCHES.find((b) => b === sourceHandle);
29423
+ edges.push({ source, target, ...branch !== void 0 ? { sourceHandle: branch } : {} });
29424
+ }
29425
+ return { nodes, edges };
29426
+ }
29427
+ function emailConfigOf(node) {
29428
+ const raw = node.config.email;
29429
+ if (!isRecord2(raw)) return null;
29430
+ return {
29431
+ templateId: optionalString(raw.templateId) ?? null,
29432
+ templateName: optionalString(raw.templateName) ?? null,
29433
+ subject: optionalString(raw.subject),
29434
+ html: optionalString(raw.html)
29435
+ };
29436
+ }
29437
+ async function resolveTemplateNames(client, graphs) {
29438
+ const ids = /* @__PURE__ */ new Set();
29439
+ for (const graph of graphs) {
29440
+ for (const node of graph?.nodes ?? []) {
29441
+ const templateId = emailConfigOf(node)?.templateId;
29442
+ if (templateId) ids.add(templateId);
29443
+ }
29444
+ }
29445
+ const names = /* @__PURE__ */ new Map();
29446
+ await Promise.all(
29447
+ [...ids].map(async (id) => {
29448
+ try {
29449
+ const { email } = await client.getLibraryEmail(id);
29450
+ names.set(id, email.name);
29451
+ } catch {
29452
+ }
29453
+ })
29454
+ );
29455
+ return names;
29456
+ }
29457
+ function printWorkflowGraphs(workflow, published, draft, templateNames) {
29458
+ if (workflow.draftGraph === void 0 && workflow.publishedGraph === void 0) {
29459
+ process.stdout.write(
29460
+ "\nThis API does not return workflow graphs yet (it predates `?include=graph`) \u2014 update the API to see the steps.\n"
29461
+ );
29462
+ return;
29463
+ }
29464
+ const differs = workflow.hasUnpublishedChanges === true;
29465
+ if (published) {
29466
+ process.stdout.write(
29467
+ `
29468
+ Graph shown: published (what runs now)${differs ? " \u2014 the draft has unpublished changes, listed below" : ""}
29469
+ `
29470
+ );
29471
+ printGraphWalk(published, templateNames);
29472
+ if (differs && draft) {
29473
+ process.stdout.write("\nDraft graph (unpublished changes \u2014 not running):\n");
29474
+ printGraphWalk(draft, templateNames);
29475
+ }
29476
+ return;
29477
+ }
29478
+ if (draft) {
29479
+ process.stdout.write("\nGraph shown: draft (never published \u2014 nothing runs yet)\n");
29480
+ printGraphWalk(draft, templateNames);
29481
+ return;
29482
+ }
29483
+ process.stdout.write("\n(no graph)\n");
29484
+ }
29485
+ function printGraphWalk(graph, templateNames) {
29486
+ const lines = walkGraph(graph, templateNames);
29487
+ if (lines.length === 0) {
29488
+ process.stdout.write(" (empty graph)\n");
29489
+ return;
29490
+ }
29491
+ for (const line of lines) process.stdout.write(line.length > 0 ? ` ${line}
29492
+ ` : "\n");
29493
+ }
29494
+ function walkGraph(graph, templateNames) {
29495
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
29496
+ const outgoing = /* @__PURE__ */ new Map();
29497
+ for (const edge of graph.edges) {
29498
+ const list = outgoing.get(edge.source) ?? [];
29499
+ list.push(edge);
29500
+ outgoing.set(edge.source, list);
29501
+ }
29502
+ const lines = [];
29503
+ const reached = /* @__PURE__ */ new Set();
29504
+ const visit = (nodeId, depth, path2) => {
29505
+ const node = nodesById.get(nodeId);
29506
+ const indent = " ".repeat(depth);
29507
+ if (!node) {
29508
+ lines.push(`${indent}-> (missing node ${nodeId})`);
29509
+ return;
29510
+ }
29511
+ if (path2.has(nodeId)) {
29512
+ lines.push(`${indent}-> loops back to ${describeNodeRef(node)}`);
29513
+ return;
29514
+ }
29515
+ reached.add(nodeId);
29516
+ lines.push(`${indent}${describeNode(node, templateNames)}`);
29517
+ const nextPath = new Set(path2).add(nodeId);
29518
+ const edges = outgoing.get(nodeId) ?? [];
29519
+ if (node.kind === "condition") {
29520
+ for (const branch of GRAPH_CONDITION_BRANCHES) {
29521
+ const branchEdges = edges.filter((edge) => edge.sourceHandle === branch);
29522
+ lines.push(
29523
+ `${indent} ${branch === "true" ? "yes" : "no"} ->${branchEdges.length === 0 ? " (end)" : ""}`
29524
+ );
29525
+ for (const edge of branchEdges) visit(edge.target, depth + 2, nextPath);
29526
+ }
29527
+ return;
29528
+ }
29529
+ for (const edge of edges) visit(edge.target, depth + (edges.length > 1 ? 1 : 0), nextPath);
29530
+ };
29531
+ const triggers = graph.nodes.filter((node) => node.kind === "trigger");
29532
+ triggers.forEach((trigger, index) => {
29533
+ if (index > 0) lines.push("");
29534
+ visit(trigger.id, 0, /* @__PURE__ */ new Set());
29535
+ });
29536
+ const orphans = graph.nodes.filter((node) => !reached.has(node.id));
29537
+ if (orphans.length > 0) {
29538
+ if (lines.length > 0) lines.push("");
29539
+ lines.push("Not reachable from any trigger (never runs):");
29540
+ for (const node of orphans) lines.push(` ${describeNode(node, templateNames)}`);
29541
+ }
29542
+ return lines;
29543
+ }
29544
+ function describeNodeRef(node) {
29545
+ return node.name ? `"${node.name}" (${node.id})` : node.id;
29546
+ }
29547
+ function describeNode(node, templateNames) {
29548
+ const prefix = `[${node.kind}${node.disabled ? ", disabled" : ""}]`;
29549
+ const label = node.name ? ` "${node.name}"` : "";
29550
+ const suffix = ` (${node.id})`;
29551
+ switch (node.kind) {
29552
+ case "trigger":
29553
+ return `${prefix}${label} ${describeTrigger(node.config)}${suffix}`;
29554
+ case "action":
29555
+ return `${prefix}${label} ${describeAction(node, templateNames)}${suffix}`;
29556
+ case "condition":
29557
+ return `${prefix}${label} if ${describeCondition2(node.config)}${suffix}`;
29558
+ case "delay":
29559
+ return `${prefix}${label} ${describeDelay(node.config)}${suffix}`;
29560
+ }
29561
+ }
29562
+ function describeTrigger(config) {
29563
+ const eventType = optionalString(config.eventType) ?? "(no event type)";
29564
+ const scopes = [];
29565
+ const scopeOf2 = (key, label) => {
29566
+ const ids = config[key];
29567
+ if (Array.isArray(ids) && ids.length > 0) scopes.push(`${label}: ${ids.join(", ")}`);
29568
+ };
29569
+ scopeOf2("productIds", "products");
29570
+ scopeOf2("formIds", "forms");
29571
+ scopeOf2("tagIds", "tags");
29572
+ if (typeof config.inactiveDays === "number") scopes.push(`inactive ${config.inactiveDays} days`);
29573
+ return scopes.length > 0 ? `${eventType} \u2014 ${scopes.join("; ")}` : eventType;
29574
+ }
29575
+ function isKnownActionType(value) {
29576
+ return typeof value === "string" && value in GRAPH_ACTION_LABELS;
29577
+ }
29578
+ function describeAction(node, templateNames) {
29579
+ const { taskType } = node.config;
29580
+ if (!isKnownActionType(taskType)) {
29581
+ const inner = isRecord2(node.config.action) ? optionalString(node.config.action.type) : void 0;
29582
+ return `${typeof taskType === "string" ? taskType : "action"}${inner ? ` ${inner}` : ""}`;
29583
+ }
29584
+ const label = GRAPH_ACTION_LABELS[taskType];
29585
+ switch (taskType) {
29586
+ case "sendEmail":
29587
+ return `${label} \u2014 ${describeEmail(node, templateNames)}`;
29588
+ case "notifyOwner": {
29589
+ const owner = isRecord2(node.config.ownerNotification) ? node.config.ownerNotification : {};
29590
+ return `${label} \u2014 subject: ${quote(optionalString(owner.subject))} \u2014 body: ${quote(excerptHtml(optionalString(owner.html)))}`;
29591
+ }
29592
+ case "sendSms": {
29593
+ const sms = isRecord2(node.config.sms) ? node.config.sms : {};
29594
+ return `${label} \u2014 text: ${quote(excerptText(optionalString(sms.text)))}`;
29595
+ }
29596
+ case "webhook": {
29597
+ const webhook = isRecord2(node.config.webhook) ? node.config.webhook : {};
29598
+ const method = optionalString(webhook.method) ?? optionalString(webhook.type);
29599
+ return `${label} \u2014 ${method ? `${method.toUpperCase()} ` : ""}${optionalString(webhook.url) ?? "(no url)"}`;
29600
+ }
29601
+ case "publishSocialPost": {
29602
+ const social = isRecord2(node.config.social) ? node.config.social : {};
29603
+ const targets = Array.isArray(social.targets) ? social.targets.join(", ") : "";
29604
+ return `${label}${optionalString(social.mode) ? ` (${social.mode})` : ""}${targets ? ` to ${targets}` : ""} \u2014 text: ${quote(excerptText(optionalString(social.text)))}`;
29605
+ }
29606
+ case "createCommunityPost": {
29607
+ const post = isRecord2(node.config.communityPost) ? node.config.communityPost : {};
29608
+ const title = optionalString(post.title);
29609
+ return `${label} in channel ${optionalString(post.channelId) ?? "?"}${title ? ` \u2014 title: ${quote(title)}` : ""} \u2014 text: ${quote(excerptText(optionalString(post.text)))}`;
29610
+ }
29611
+ case "updateClientTags": {
29612
+ const tags = isRecord2(node.config.clientTags) ? node.config.clientTags : {};
29613
+ const ids = Array.isArray(tags.tagIds) ? tags.tagIds.join(", ") : "";
29614
+ return `${label} \u2014 ${optionalString(tags.mode) ?? "update"} ${ids || "(no tags)"}`;
29615
+ }
29616
+ case "setEmailSubscription": {
29617
+ const sub = isRecord2(node.config.emailSubscription) ? node.config.emailSubscription : {};
29618
+ return `${label} \u2014 ${optionalString(sub.status) ?? "?"}`;
29619
+ }
29620
+ case "openClientDeadline": {
29621
+ const deadline = isRecord2(node.config.clientDeadline) ? node.config.clientDeadline : {};
29622
+ const amount = typeof deadline.amount === "number" ? String(deadline.amount) : "?";
29623
+ const endTime = optionalString(deadline.endTime);
29624
+ return `${label} \u2014 ${optionalString(deadline.key) ?? "?"} for ${amount} ${optionalString(deadline.unit) ?? "?"}${endTime ? `, closes at ${endTime}` : ""}`;
29625
+ }
29626
+ case "requestTestimonial":
29627
+ return label;
29628
+ }
29629
+ }
29630
+ function describeEmail(node, templateNames) {
29631
+ const email = emailConfigOf(node);
29632
+ if (!email) return "(no email config)";
29633
+ if (email.templateId) {
29634
+ const name = templateNames.get(email.templateId) ?? email.templateName ?? null;
29635
+ return `library email ${name ? `${quote(name)} ` : ""}(${email.templateId}) \u2014 sent as saved in the library`;
29636
+ }
29637
+ return `subject: ${quote(email.subject)} \u2014 body: ${quote(excerptHtml(email.html))}`;
29638
+ }
29639
+ function describeCondition2(config) {
29640
+ const rules = Array.isArray(config.rules) ? config.rules.filter(isRecord2) : [];
29641
+ if (rules.length === 0) return "(no rules)";
29642
+ return rules.map(describeRule).join(" AND ");
29643
+ }
29644
+ function describeRule(rule) {
29645
+ const field = optionalString(rule.field) ?? "?";
29646
+ const details = [];
29647
+ for (const key of ["productId", "tagId", "consentName", "days"]) {
29648
+ const value = rule[key];
29649
+ if (typeof value === "string" || typeof value === "number") details.push(`${key}=${value}`);
29650
+ }
29651
+ const detail = details.length > 0 ? ` (${details.join(", ")})` : "";
29652
+ const op = optionalString(rule.op);
29653
+ if (op !== void 0) {
29654
+ const value = Array.isArray(rule.values) ? `[${rule.values.join(", ")}]` : typeof rule.value === "string" || typeof rule.value === "number" ? String(rule.value) : "?";
29655
+ return `${field} ${op} ${value}${detail}`;
29656
+ }
29657
+ if (typeof rule.expected === "boolean") return `${field}${detail} is ${rule.expected}`;
29658
+ return `${field}${detail}`;
29659
+ }
29660
+ function describeDelay(config) {
29661
+ const mode = optionalString(config.mode);
29662
+ switch (mode) {
29663
+ case void 0:
29664
+ case "duration":
29665
+ return `wait ${typeof config.amount === "number" ? config.amount : "?"} ${optionalString(config.unit) ?? ""}`.trimEnd();
29666
+ case "untilWeekday":
29667
+ return `wait until weekday ${typeof config.weekday === "number" ? config.weekday : "?"} at ${optionalString(config.time) ?? "?"}${optionalString(config.timezone) ? ` (${config.timezone})` : ""}`;
29668
+ case "untilDate":
29669
+ return `wait until ${optionalString(config.date) ?? "?"}${optionalString(config.timezone) ? ` (${config.timezone})` : ""}`;
29670
+ case "untilContextDate": {
29671
+ const offset = typeof config.offsetMinutes === "number" ? config.offsetMinutes : 0;
29672
+ return `wait until ${optionalString(config.contextKey) ?? "?"}${offset !== 0 ? ` ${offset > 0 ? "+" : ""}${offset} min` : ""}${config.skipWhenPast === true ? " (skip branch when past)" : ""}`;
29673
+ }
29674
+ default:
29675
+ return `wait (${mode})`;
29676
+ }
29677
+ }
29678
+ function quote(value) {
29679
+ return value !== void 0 && value.length > 0 ? `"${value}"` : "(empty)";
29680
+ }
29681
+ function excerptHtml(html) {
29682
+ if (html === void 0) return void 0;
29683
+ const text = html.replace(/<(style|script|head)\b[^>]*>[\s\S]*?<\/\1>/gi, " ").replace(/<br\s*\/?>|<\/(p|div|li|h[1-6]|tr)>/gi, " ").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;|&apos;/g, "'");
29684
+ return excerptText(text);
29685
+ }
29686
+ function excerptText(text) {
29687
+ if (text === void 0) return void 0;
29688
+ const collapsed = text.replace(/\s+/g, " ").trim();
29689
+ return collapsed.length > EXCERPT_LENGTH ? `${collapsed.slice(0, EXCERPT_LENGTH - 1)}\u2026` : collapsed;
29690
+ }
27243
29691
 
27244
29692
  // src/commands/workspace/index.ts
27245
- function parseOnOff3(value, flag) {
29693
+ function parseOnOff5(value, flag) {
27246
29694
  const match = ["on", "off"].find((v) => v === value);
27247
29695
  if (!match) throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
27248
29696
  return match === "on";
@@ -27310,7 +29758,7 @@ function registerWorkspaceCommands(program2) {
27310
29758
  }
27311
29759
  if (options.inheritTimezone) input.timezone = null;
27312
29760
  if (options.autoTranscribe !== void 0) {
27313
- input.autoTranscribeUploads = parseOnOff3(options.autoTranscribe, "--auto-transcribe");
29761
+ input.autoTranscribeUploads = parseOnOff5(options.autoTranscribe, "--auto-transcribe");
27314
29762
  }
27315
29763
  if (options.transcriptionLanguage !== void 0) {
27316
29764
  input.transcriptionLanguage = parseTranscriptionLanguage(options.transcriptionLanguage);
@@ -27327,13 +29775,65 @@ function registerWorkspaceCommands(program2) {
27327
29775
  );
27328
29776
  }
27329
29777
 
29778
+ // src/utils/shortHelp.ts
29779
+ var SHORT_HELP_FLAG = "--short";
29780
+ var SHORT_HELP_DESCRIPTION = "with --help: print only the command names (subcommands indented), no descriptions";
29781
+ var INDENT = " ";
29782
+ function rootOf(command) {
29783
+ let current = command;
29784
+ while (current.parent) current = current.parent;
29785
+ return current;
29786
+ }
29787
+ function rawArgsOf(command) {
29788
+ const root = rootOf(command);
29789
+ if ("rawArgs" in root && Array.isArray(root.rawArgs)) {
29790
+ return root.rawArgs.filter((arg) => typeof arg === "string");
29791
+ }
29792
+ return [];
29793
+ }
29794
+ function isShortHelpRequested(command) {
29795
+ return rawArgsOf(command).includes(SHORT_HELP_FLAG);
29796
+ }
29797
+ function visibleSubcommands(command, helper) {
29798
+ return helper.visibleCommands(command).filter((sub) => command.commands.includes(sub));
29799
+ }
29800
+ function formatShortHelp(command, helper) {
29801
+ const lines = [];
29802
+ const walk = (cmd, depth) => {
29803
+ lines.push(`${INDENT.repeat(depth)}${cmd.name()}`);
29804
+ for (const sub of visibleSubcommands(cmd, helper)) walk(sub, depth + 1);
29805
+ };
29806
+ if (command.parent === null) {
29807
+ for (const sub of visibleSubcommands(command, helper)) walk(sub, 0);
29808
+ } else {
29809
+ walk(command, 0);
29810
+ }
29811
+ return `${lines.join("\n")}
29812
+ `;
29813
+ }
29814
+ function registerOnCommand2(command) {
29815
+ if (!command.options.some((option) => option.long === SHORT_HELP_FLAG)) {
29816
+ command.addOption(new Option(SHORT_HELP_FLAG, SHORT_HELP_DESCRIPTION).hideHelp());
29817
+ }
29818
+ const inherited = command.configureHelp();
29819
+ const previousFormatHelp = inherited.formatHelp;
29820
+ command.configureHelp({
29821
+ ...inherited,
29822
+ formatHelp: (cmd, helper) => isShortHelpRequested(cmd) ? formatShortHelp(cmd, helper) : previousFormatHelp ? previousFormatHelp.call(helper, cmd, helper) : Help.prototype.formatHelp.call(helper, cmd, helper)
29823
+ });
29824
+ for (const sub of command.commands) registerOnCommand2(sub);
29825
+ }
29826
+ function applyShortHelp(program2) {
29827
+ registerOnCommand2(program2);
29828
+ }
29829
+
27330
29830
  // src/program.ts
27331
29831
  function resolveCliVersion() {
27332
- if ("0.3.47".length > 0) {
27333
- return "0.3.47";
29832
+ if ("0.3.63".length > 0) {
29833
+ return "0.3.63";
27334
29834
  }
27335
29835
  try {
27336
- const raw = readFileSync26(new URL("../package.json", import.meta.url), "utf8");
29836
+ const raw = readFileSync28(new URL("../package.json", import.meta.url), "utf8");
27337
29837
  const parsed = JSON.parse(raw);
27338
29838
  if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
27339
29839
  const { version: version2 } = parsed;
@@ -27376,6 +29876,7 @@ Signing in \u2014 two roles, two consent pages:
27376
29876
  registerProductsCommands(program2);
27377
29877
  registerOffersCommands(program2);
27378
29878
  registerCheckoutSettingsCommands(program2);
29879
+ registerIntegrationsCommands(program2);
27379
29880
  registerSiteCommands(program2);
27380
29881
  registerEmailsCommands(program2);
27381
29882
  registerOtoCommands(program2);
@@ -27390,6 +29891,7 @@ Signing in \u2014 two roles, two consent pages:
27390
29891
  registerSmsCampaignsCommands(program2);
27391
29892
  registerSegmentsCommands(program2);
27392
29893
  registerFormsCommands(program2);
29894
+ registerLiveRoomsCommands(program2);
27393
29895
  registerTestimonialsCommands(program2);
27394
29896
  registerBrandCommands(program2);
27395
29897
  registerTeamCommands(program2);
@@ -27406,6 +29908,7 @@ Signing in \u2014 two roles, two consent pages:
27406
29908
  registerSocialCommands(program2);
27407
29909
  registerMediaCommands(program2);
27408
29910
  applyGlobalOptions(program2);
29911
+ applyShortHelp(program2);
27409
29912
  return program2;
27410
29913
  }
27411
29914