@zanfia/cli 0.3.47 → 0.3.52

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 +2308 -360
  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
  },
@@ -8933,7 +9030,10 @@ function createHttpClient(config) {
8933
9030
  "GET",
8934
9031
  productId ? `/checkouts?productId=${encodeURIComponent(productId)}` : "/checkouts"
8935
9032
  ),
8936
- getCheckout: (id) => request("GET", `/checkouts/${encodeURIComponent(id)}`),
9033
+ getCheckout: (id, options) => request(
9034
+ "GET",
9035
+ options?.full ? `/checkouts/${encodeURIComponent(id)}?include=full` : `/checkouts/${encodeURIComponent(id)}`
9036
+ ),
8937
9037
  createCheckout: (input) => request("POST", "/checkouts", input),
8938
9038
  updateCheckout: (id, input) => request("PATCH", `/checkouts/${encodeURIComponent(id)}`, input),
8939
9039
  publishCheckout: (id, active) => request("POST", `/checkouts/${encodeURIComponent(id)}/publish`, { active }),
@@ -9195,6 +9295,33 @@ function createHttpClient(config) {
9195
9295
  "GET",
9196
9296
  `/conversion-tracking/health${sinceDays !== void 0 ? `?sinceDays=${sinceDays}` : ""}`
9197
9297
  ),
9298
+ getIntegrationsOverview: () => request("GET", "/integrations"),
9299
+ connectEmailClient: (input) => request("POST", "/integrations/email-clients", input),
9300
+ disconnectEmailClient: (client, url) => request("POST", "/integrations/email-clients/disconnect", { client, url }),
9301
+ refreshEmailClientLists: (input) => request("POST", "/integrations/email-clients/refresh-lists", input),
9302
+ connectMetaConversionApi: (input) => request("PUT", "/integrations/meta-conversion-api", input),
9303
+ disconnectMetaConversionApi: (integrationId) => request("DELETE", `/integrations/meta-conversion-api/${encodeURIComponent(integrationId)}`),
9304
+ configureManualPayment: (input) => request("PUT", "/integrations/payments/manual", input),
9305
+ disconnectManualPayment: () => request("DELETE", "/integrations/payments/manual"),
9306
+ connectInvoicesClient: (input) => request("POST", "/integrations/invoices-clients", input),
9307
+ disconnectInvoicesClient: (client, id) => request("POST", "/integrations/invoices-clients/disconnect", { client, id }),
9308
+ connectPaymentGateway: (input) => request("POST", "/integrations/payments/gateways", input),
9309
+ disconnectPaymentGateway: (gateway) => request("POST", "/integrations/payments/gateways/disconnect", { gateway }),
9310
+ setTpayRecurringBlik: (enabled) => request("PUT", "/integrations/payments/tpay/recurring-blik", { enabled }),
9311
+ connectTwilio: (input) => request("PUT", "/integrations/sms/twilio", input),
9312
+ disconnectTwilio: () => request("DELETE", "/integrations/sms/twilio"),
9313
+ connectSmsApi: (input) => request("PUT", "/integrations/sms/smsapi", input),
9314
+ disconnectSmsApi: () => request("DELETE", "/integrations/sms/smsapi"),
9315
+ refreshSmsApiSenders: () => request("POST", "/integrations/sms/smsapi/refresh-senders"),
9316
+ setDefaultSmsApiSender: (sender) => request("PUT", "/integrations/sms/smsapi/default-sender", { sender }),
9317
+ setDefaultSmsProvider: (provider) => request("PUT", "/integrations/sms/default-provider", { provider }),
9318
+ listConversionTrackingIntegrations: () => request("GET", "/conversion-tracking/integrations"),
9319
+ upsertConversionTrackingIntegration: (input) => request("PUT", "/conversion-tracking/integrations", input),
9320
+ removeConversionTrackingIntegration: (client, integrationId) => request(
9321
+ "DELETE",
9322
+ `/conversion-tracking/integrations/${encodeURIComponent(client)}/${encodeURIComponent(integrationId)}`
9323
+ ),
9324
+ setDefaultConversionTracking: (integrationIds) => request("PUT", "/conversion-tracking/defaults", { integrationIds: [...integrationIds] }),
9198
9325
  statsProduct: (productId, currency) => request("GET", `/products/${encodeURIComponent(productId)}/stats${statsQuery({ currency })}`),
9199
9326
  listCommunities: () => communityRequest("GET", "/v1/communities"),
9200
9327
  listCommunityChannels: (communityId) => communityRequest("GET", `/v1/communities/${encodeURIComponent(communityId)}/channels`),
@@ -11787,10 +11914,10 @@ function registerBrandCommands(program2) {
11787
11914
  });
11788
11915
  }
11789
11916
  async function buildUpdatePatch(options) {
11790
- const { readFile: readFile3 } = await import("fs/promises");
11917
+ const { readFile: readFile4 } = await import("fs/promises");
11791
11918
  let patch = {};
11792
11919
  if (options.jsonFile) {
11793
- const raw = await readFile3(options.jsonFile, "utf8");
11920
+ const raw = await readFile4(options.jsonFile, "utf8");
11794
11921
  const parsed = JSON.parse(raw);
11795
11922
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
11796
11923
  throw new Error("--json-file must contain a JSON object with name/visual/editorial keys.");
@@ -11805,9 +11932,42 @@ async function buildUpdatePatch(options) {
11805
11932
  }
11806
11933
 
11807
11934
  // src/commands/bumps/index.ts
11935
+ var IMAGE_MEDIA_TYPE = "image";
11936
+ var CLEAR_MEDIA_VALUE = "none";
11937
+ var HTTP_URL_PATTERN = /^https?:\/\//i;
11938
+ var IMAGE_MIME_PREFIX = "image/";
11939
+ 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.";
11940
+ 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
11941
  function formatAmount(amountCents, currency) {
11809
11942
  return `${(amountCents / 100).toFixed(2)} ${currency}`;
11810
11943
  }
11944
+ function parseBumpJsonPatch(value) {
11945
+ return parseJsonObjectFileOrInline("--json")(value);
11946
+ }
11947
+ async function resolveImageMedia(client, value) {
11948
+ const trimmed = value.trim();
11949
+ if (trimmed.toLowerCase() === CLEAR_MEDIA_VALUE) return null;
11950
+ if (HTTP_URL_PATTERN.test(trimmed)) {
11951
+ return { type: IMAGE_MEDIA_TYPE, url: trimmed, mediaId: null };
11952
+ }
11953
+ const { media } = await client.getMedia(trimmed);
11954
+ if (!media.mimeType.startsWith(IMAGE_MIME_PREFIX)) {
11955
+ throw new Error(
11956
+ `--image: media ${trimmed} is ${media.mimeType}, not an image. Pass an image media id / URL, or set a video slot via --json {"appearance":{"media":{...}}}.`
11957
+ );
11958
+ }
11959
+ return { type: IMAGE_MEDIA_TYPE, url: media.url, mediaId: media.id };
11960
+ }
11961
+ function mergeBumpInput(jsonPatch, flagInput, media) {
11962
+ const merged = { ...jsonPatch, ...flagInput };
11963
+ for (const [key, value] of Object.entries(flagInput)) {
11964
+ if (value === void 0) delete merged[key];
11965
+ }
11966
+ const appearance = media !== void 0 ? { ...jsonPatch.appearance ?? {}, media } : jsonPatch.appearance;
11967
+ if (appearance !== void 0) merged.appearance = appearance;
11968
+ else delete merged.appearance;
11969
+ return merged;
11970
+ }
11811
11971
  function parsePriceIds(value) {
11812
11972
  const ids = value.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
11813
11973
  if (ids.length === 0) {
@@ -11834,39 +11994,52 @@ function registerBumpsCommands(program2) {
11834
11994
  );
11835
11995
  });
11836
11996
  });
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(
11997
+ 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
11998
  async (options, command) => {
11839
11999
  const flags = getGlobalFlags(command);
11840
12000
  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
- });
12001
+ const jsonPatch = options.json !== void 0 ? parseBumpJsonPatch(options.json) : {};
12002
+ const media = options.image !== void 0 ? await resolveImageMedia(client, options.image) : void 0;
12003
+ const input = mergeBumpInput(
12004
+ jsonPatch,
12005
+ {
12006
+ productId: options.product,
12007
+ priceId: options.price,
12008
+ name: options.name,
12009
+ description: options.description,
12010
+ ctaText: options.cta
12011
+ },
12012
+ media
12013
+ );
12014
+ const result = await client.createOrderBump(input);
11848
12015
  emit(flags, result, () => {
11849
12016
  process.stdout.write(`Created order bump ${result.orderBumpId} \u2014 ${options.name}
11850
12017
  `);
11851
12018
  });
11852
12019
  }
11853
12020
  );
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(
12021
+ 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
12022
  async (bumpId, options, command) => {
11856
12023
  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)) {
12024
+ const jsonPatch = options.json !== void 0 ? parseBumpJsonPatch(options.json) : {};
12025
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12026
+ const media = options.image !== void 0 ? await resolveImageMedia(client, options.image) : void 0;
12027
+ const input = mergeBumpInput(
12028
+ jsonPatch,
12029
+ {
12030
+ name: options.name,
12031
+ description: options.description,
12032
+ ctaText: options.cta,
12033
+ productId: options.product,
12034
+ priceId: options.price
12035
+ },
12036
+ media
12037
+ );
12038
+ if (Object.keys(input).length === 0) {
11865
12039
  throw new Error(
11866
- "Nothing to update \u2014 pass at least one of --name, --description, --cta, --product, --price."
12040
+ "Nothing to update \u2014 pass at least one of --name, --description, --cta, --product, --price, --image, or a --json patch."
11867
12041
  );
11868
12042
  }
11869
- const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11870
12043
  const result = await client.updateOrderBump(bumpId, input);
11871
12044
  emit(flags, result, () => {
11872
12045
  process.stdout.write(`Updated order bump ${result.orderBumpId}
@@ -12401,7 +12574,19 @@ function registerCheckoutSettingsCommands(program2) {
12401
12574
  ).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
12575
  "--claim <text|none>",
12403
12576
  "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(
12577
+ ).option("--terms-content <text>", "inline terms & conditions text").option("--privacy-policy-content <text>", "inline privacy policy text").option(
12578
+ "--google-analytics-id <id|none>",
12579
+ "LEGACY Google Analytics id (none = clear) \u2014 prefer `integrations add ga4`"
12580
+ ).option(
12581
+ "--facebook-pixel-id <id|none>",
12582
+ "LEGACY Facebook Pixel id (none = clear) \u2014 Meta pixel is managed in the dashboard"
12583
+ ).option(
12584
+ "--gtm-id <id|none>",
12585
+ "LEGACY Google Tag Manager id (none = clear) \u2014 prefer `integrations add gtm`"
12586
+ ).option(
12587
+ "--tiktok-pixel-id <id|none>",
12588
+ "LEGACY TikTok Pixel id (none = clear) \u2014 prefer `integrations add tiktok`"
12589
+ ).option("--promote-net-prices <on|off>", "display net prices prominently").option(
12405
12590
  "--price-variants-placement <belowDescription|aboveDescription|none>",
12406
12591
  "where the price list renders relative to the description"
12407
12592
  ).option(
@@ -12536,6 +12721,39 @@ var AIImageQuality = /* @__PURE__ */ ((AIImageQuality2) => {
12536
12721
  AIImageQuality2["Ultra"] = "4K";
12537
12722
  return AIImageQuality2;
12538
12723
  })(AIImageQuality || {});
12724
+ var MANAGED_CONVERSION_TRACKING_CLIENTS = [
12725
+ "googleAnalytics",
12726
+ "googleTagManager",
12727
+ "tiktok",
12728
+ "x",
12729
+ "linkedin",
12730
+ "microsoftClarity",
12731
+ "hotjar"
12732
+ ];
12733
+ var CONNECTABLE_EMAIL_CLIENTS = [
12734
+ "ActiveCampaign",
12735
+ "ConvertKit",
12736
+ "MailerLite",
12737
+ "MailChimp",
12738
+ "GetResponse",
12739
+ "Pipedrive"
12740
+ ];
12741
+ var INVOICES_CLIENTS = [
12742
+ "fakturownia",
12743
+ "ifirma",
12744
+ "inFakt",
12745
+ "wFirmaV2",
12746
+ "ingKsiegowosc",
12747
+ "stripe"
12748
+ ];
12749
+ var CREDENTIAL_PAYMENT_GATEWAYS = [
12750
+ "payu",
12751
+ "tpay",
12752
+ "przelewy24",
12753
+ "paynow",
12754
+ "paypal",
12755
+ "dodo"
12756
+ ];
12539
12757
  var COMMUNITY_EVENT_LOCATION_TYPES = [
12540
12758
  "online",
12541
12759
  "inPerson"
@@ -17086,6 +17304,36 @@ function printSignupFormDetail(form) {
17086
17304
  `);
17087
17305
  }
17088
17306
  }
17307
+ var escapeHtmlAttribute = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
17308
+ function buildFormSnippet(input) {
17309
+ const copy = input.lang === "pl" ? {
17310
+ email: "Adres e-mail",
17311
+ firstName: "Imi\u0119",
17312
+ submit: "Zapisuj\u0119 si\u0119",
17313
+ success: "Dzi\u0119kujemy! Sprawd\u017A skrzynk\u0119 i potwierd\u017A zapis.",
17314
+ error: "Nie uda\u0142o si\u0119 wys\u0142a\u0107. Spr\xF3buj ponownie."
17315
+ } : {
17316
+ email: "E-mail address",
17317
+ firstName: "First name",
17318
+ submit: "Sign me up",
17319
+ success: "Thank you! Check your inbox to confirm.",
17320
+ error: "Something went wrong. Please try again."
17321
+ };
17322
+ const redirect = input.redirectUrl ? ` data-zanfia-redirect="${escapeHtmlAttribute(input.redirectUrl)}"` : "";
17323
+ const firstName = input.firstName ? `
17324
+ <label>${copy.firstName}<br><input name="firstName" type="text" autocomplete="given-name"></label>` : "";
17325
+ return [
17326
+ `<form data-zanfia-form="${input.workspaceId}/${input.formId}"${redirect}>`,
17327
+ ` <label>${copy.email}<br><input name="email" type="email" required autocomplete="email"></label>${firstName}`,
17328
+ ` <input name="website" type="text" tabindex="-1" autocomplete="off" aria-hidden="true" style="position:absolute;left:-9999px;opacity:0">`,
17329
+ ` <button type="submit">${copy.submit}</button>`,
17330
+ ` <div data-zanfia-success hidden>${copy.success}</div>`,
17331
+ ` <div data-zanfia-error hidden>${copy.error}</div>`,
17332
+ `</form>`,
17333
+ `<script src="${input.platformUrl}/zanfia-page-forms.js" defer></script>`,
17334
+ ""
17335
+ ].join("\n");
17336
+ }
17089
17337
  async function buildFormUpdateInput(client, formId, options) {
17090
17338
  const input = options.json !== void 0 ? parseFormJsonPatch(options.json) : {};
17091
17339
  if (options.name !== void 0) input.name = options.name;
@@ -17221,6 +17469,45 @@ function registerFormsCommands(program2) {
17221
17469
  `);
17222
17470
  });
17223
17471
  });
17472
+ forms.command("snippet").description(
17473
+ "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."
17474
+ ).argument("<id>", "form id").option("--lang <pl|en>", "language of the placeholder copy (default pl)").option("--first-name", "include a first-name field").option(
17475
+ "--redirect <url>",
17476
+ "thank-you URL for THIS embed (overrides the form setting on the page)"
17477
+ ).option(
17478
+ "--platform-url <origin>",
17479
+ "origin that serves zanfia-page-forms.js (default: this build\u2019s platform, or $ZANFIA_PLATFORM_URL)"
17480
+ ).action(
17481
+ async (id, options, command) => {
17482
+ const flags = getGlobalFlags(command);
17483
+ const lang = options.lang !== void 0 ? parseChoice2(options.lang, ["pl", "en"], "--lang") : "pl";
17484
+ if (options.redirect !== void 0 && !/^https?:\/\/\S+$/.test(options.redirect)) {
17485
+ throw new Error("--redirect must be an absolute http(s) URL.");
17486
+ }
17487
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
17488
+ const [form, workspace2] = await Promise.all([
17489
+ client.getSignupForm(id),
17490
+ client.getWorkspaceSettings()
17491
+ ]);
17492
+ if (form.submitAction === "freeProductSignup") {
17493
+ throw new Error(
17494
+ `Form ${id} is product-bound; the native snippet works for standalone signup forms only.`
17495
+ );
17496
+ }
17497
+ const platformUrl = (options.platformUrl ?? resolvePlatformUrl()).replace(/\/+$/, "");
17498
+ const snippet = buildFormSnippet({
17499
+ workspaceId: workspace2.workspaceId,
17500
+ formId: form.id,
17501
+ platformUrl,
17502
+ lang,
17503
+ redirectUrl: options.redirect ?? form.redirectUrl ?? null,
17504
+ firstName: options.firstName === true
17505
+ });
17506
+ emit(flags, { formId: form.id, workspaceId: workspace2.workspaceId, html: snippet }, () => {
17507
+ process.stdout.write(snippet);
17508
+ });
17509
+ }
17510
+ );
17224
17511
  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
17512
  const flags = getGlobalFlags(command);
17226
17513
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -17258,6 +17545,7 @@ var FUNNEL_TEMPLATE_IDS = [
17258
17545
  ];
17259
17546
  var EVENT_MODES = ["live", "on_demand"];
17260
17547
  var SCHEDULER_UNAVAILABLE_STATUS2 = 503;
17548
+ var FUNNEL_NOT_READY_STATUS = 409;
17261
17549
  function requireConfirm6(confirm, what) {
17262
17550
  if (!confirm) {
17263
17551
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
@@ -17273,6 +17561,16 @@ function withSchedulerHint2(error) {
17273
17561
  }
17274
17562
  throw error;
17275
17563
  }
17564
+ function withLiveGateHint(error, funnelId) {
17565
+ if (error instanceof ApiError && error.status === FUNNEL_NOT_READY_STATUS) {
17566
+ throw new ApiError(
17567
+ error.status,
17568
+ error.apiMessage,
17569
+ `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.`
17570
+ );
17571
+ }
17572
+ throw error;
17573
+ }
17276
17574
  function parsePositiveInt10(value, flag) {
17277
17575
  const n = Number.parseInt(value, 10);
17278
17576
  if (!Number.isInteger(n) || n <= 0) {
@@ -17449,19 +17747,19 @@ function registerFunnelsCommands(program2) {
17449
17747
  const flags = getGlobalFlags(command);
17450
17748
  const input = buildFunnelUpdateInput(options);
17451
17749
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
17452
- const result = await client.updateFunnel(id, input);
17750
+ const result = await client.updateFunnel(id, input).catch((error) => withLiveGateHint(error, id));
17453
17751
  emit(flags, result, () => {
17454
17752
  process.stdout.write(`Updated funnel ${id}.
17455
17753
  `);
17456
17754
  });
17457
17755
  });
17458
17756
  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."
17757
+ "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
17758
  ).argument("<id>", "funnel id").requiredOption("--set <status>", `new status (${FUNNEL_STATUSES.join(" | ")})`).action(async (id, options, command) => {
17461
17759
  const flags = getGlobalFlags(command);
17462
17760
  const status = parseChoice3(options.set, FUNNEL_STATUSES, "--set");
17463
17761
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
17464
- const result = await client.updateFunnel(id, { status });
17762
+ const result = await client.updateFunnel(id, { status }).catch((error) => withLiveGateHint(error, id));
17465
17763
  emit(flags, result, () => {
17466
17764
  process.stdout.write(`Funnel ${id} status set to ${status}.
17467
17765
  `);
@@ -17530,7 +17828,10 @@ function registerFunnelsCommands(program2) {
17530
17828
  });
17531
17829
  funnels.command("from-template").description(
17532
17830
  "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(
17831
+ ).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(
17832
+ "--page-template <id>",
17833
+ "design template for every generated page (a page-template id, e.g. course-fresh); thank-you steps take the companion"
17834
+ ).option("--confirm", "required \u2014 acknowledge that AI credits will be consumed").action(
17534
17835
  async (templateId, options, command) => {
17535
17836
  const flags = getGlobalFlags(command);
17536
17837
  requireConfirm6(options.confirm, "create the funnel and generate its pages (AI credits)");
@@ -17538,7 +17839,8 @@ function registerFunnelsCommands(program2) {
17538
17839
  templateId: parseChoice3(templateId, FUNNEL_TEMPLATE_IDS, "<templateId>"),
17539
17840
  productId: options.product,
17540
17841
  lang: parseChoice3(options.lang, FUNNEL_LANGS, "--lang"),
17541
- name: options.name
17842
+ name: options.name,
17843
+ ...options.pageTemplate ? { pageTemplateId: options.pageTemplate } : {}
17542
17844
  };
17543
17845
  const webinar = {
17544
17846
  eventStartAt: options.eventStart,
@@ -17651,152 +17953,1078 @@ function registerFunnelsCommands(program2) {
17651
17953
  });
17652
17954
  }
17653
17955
 
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)) {
17956
+ // src/commands/integrations/connections.ts
17957
+ import { readFileSync as readFileSync15 } from "fs";
17958
+ var EMAIL_CLIENT_ALIASES = {
17959
+ ac: "ActiveCampaign",
17960
+ activecampaign: "ActiveCampaign",
17961
+ kit: "ConvertKit",
17962
+ convertkit: "ConvertKit",
17963
+ mailerlite: "MailerLite",
17964
+ mailchimp: "MailChimp",
17965
+ getresponse: "GetResponse",
17966
+ freshmail: "FreshMail",
17967
+ pipedrive: "Pipedrive"
17968
+ };
17969
+ function parseEmailClient(value) {
17970
+ const match = EMAIL_CLIENT_ALIASES[value.trim().toLowerCase()];
17971
+ if (!match) {
17665
17972
  throw new Error(
17666
- "Community handle must be lowercase letters, digits and dashes (e.g. crew-of-one)."
17973
+ `Unknown email client: ${value} (expected ${CONNECTABLE_EMAIL_CLIENTS.join(" | ")}; FreshMail is disconnect-only)`
17667
17974
  );
17668
17975
  }
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
- );
17976
+ return match;
17977
+ }
17978
+ function parseOnOff2(value, flag) {
17979
+ if (value === void 0) return void 0;
17980
+ if (value === "on") return true;
17981
+ if (value === "off") return false;
17982
+ throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
17983
+ }
17984
+ function requireConfirm7(confirm, what) {
17985
+ if (!confirm) {
17986
+ throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
17674
17987
  }
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.");
17988
+ }
17989
+ function readSecret(label, value, fromStdin, fromEnv) {
17990
+ const sources = [value !== void 0, fromStdin === true, fromEnv !== void 0].filter(Boolean);
17991
+ if (sources.length > 1) {
17992
+ throw new Error(`Pass ${label} through exactly one of the value, -stdin or -env forms.`);
17679
17993
  }
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
- `
17690
- );
17994
+ if (fromStdin) {
17995
+ const raw = readFileSync15(0, "utf8").trim();
17996
+ if (!raw) throw new Error(`${label}: nothing was read from stdin.`);
17997
+ return raw;
17691
17998
  }
17692
- const firstName = options.name ?? (options.json ? "" : await promptLine("Your name (optional): "));
17693
- const hostname3 = safeHostname2();
17694
- let completed = null;
17695
- for (let attempt = 1; attempt <= MAX_CODE_ATTEMPTS2 && !completed; attempt += 1) {
17696
- const code = options.code ?? (await promptLine("Code from the email (or 'r' to resend): ")).trim();
17697
- if (code.toLowerCase() === "r") {
17698
- await postJson2(
17699
- "Resend",
17700
- joinUrl(communityApiUrl, handle, "start"),
17701
- { email, lang: "en" },
17702
- { verbose }
17703
- );
17704
- process.stdout.write("Code re-sent.\n");
17705
- attempt -= 1;
17706
- continue;
17707
- }
17708
- try {
17709
- completed = await postJson2(
17710
- "Join",
17711
- joinUrl(communityApiUrl, handle, "complete"),
17712
- { email, code, firstName, hostname: hostname3 },
17713
- { verbose }
17714
- );
17715
- } catch (err) {
17716
- if (options.code || attempt === MAX_CODE_ATTEMPTS2) throw err;
17717
- process.stdout.write(`${err instanceof Error ? err.message : String(err)}
17718
- `);
17719
- }
17999
+ if (fromEnv !== void 0) {
18000
+ const raw = process.env[fromEnv]?.trim();
18001
+ if (!raw) throw new Error(`${label}: environment variable ${fromEnv} is empty or unset.`);
18002
+ return raw;
17720
18003
  }
17721
- if (!completed) {
17722
- throw new Error("Could not verify the code.");
18004
+ return value;
18005
+ }
18006
+ function printOverview(overview) {
18007
+ process.stdout.write("Email clients\n");
18008
+ if (overview.emailClients.length === 0) {
18009
+ process.stdout.write(" (none connected)\n");
18010
+ } else {
18011
+ printTable(
18012
+ overview.emailClients.map((account) => ({
18013
+ client: account.client,
18014
+ url: account.url,
18015
+ lists: account.listsCount,
18016
+ tags: account.tagsCount,
18017
+ updatedAt: account.updatedAt ?? account.createdAt
18018
+ })),
18019
+ ["client", "url", "lists", "tags", "updatedAt"]
18020
+ );
17723
18021
  }
17724
- const path2 = writeCredentials({
17725
- apiKey: completed.token,
17726
- apiUrl: communityApiUrl,
17727
- savedAt: (/* @__PURE__ */ new Date()).toISOString(),
17728
- source: "oauth",
17729
- expiresAt: completed.expiresAt,
17730
- userEmail: completed.userEmail,
17731
- workspaceId: completed.workspaceId,
17732
- workspaceName: completed.workspaceName,
17733
- kind: "member"
17734
- });
17735
- if (options.json) {
17736
- process.stdout.write(
17737
- `${JSON.stringify(
17738
- {
17739
- joined: true,
17740
- alreadyMember: completed.alreadyMember,
17741
- communityId: completed.communityId,
17742
- communityName: completed.communityName,
17743
- workspaceId: completed.workspaceId,
17744
- workspaceName: completed.workspaceName,
17745
- introChannelId: completed.introChannelId,
17746
- userEmail: completed.userEmail,
17747
- expiresAt: completed.expiresAt,
17748
- credentialsPath: path2
17749
- },
17750
- null,
17751
- 2
17752
- )}
18022
+ process.stdout.write("Meta pixel / Conversions API\n");
18023
+ if (overview.metaConversionApi.length === 0) {
18024
+ process.stdout.write(" (none)\n");
18025
+ } else {
18026
+ printMetaEntries(overview.metaConversionApi);
18027
+ }
18028
+ process.stdout.write(
18029
+ `Managed pixel integrations (GA4 / GTM / \u2026): ${overview.conversionTrackingIntegrationsCount} \u2014 see \`integrations list\`
17753
18030
  `
18031
+ );
18032
+ process.stdout.write("Invoicing\n");
18033
+ if (overview.invoicesClients.length === 0) {
18034
+ process.stdout.write(" (none connected)\n");
18035
+ } else {
18036
+ printTable(
18037
+ overview.invoicesClients.map((account) => ({
18038
+ client: account.client,
18039
+ id: account.id,
18040
+ url: account.url
18041
+ })),
18042
+ ["client", "id", "url"]
17754
18043
  );
17755
- return;
17756
18044
  }
17757
- const intro = completed.introChannelId ? ` zanfia communities post create ${completed.communityId} ${completed.introChannelId} --content "Hi, I'm <your agent>. I ..."
17758
- ` : "";
18045
+ const p = overview.payments;
18046
+ const on = (value) => value ? "on" : "off";
17759
18047
  process.stdout.write(
17760
- `
17761
- ${completed.alreadyMember ? "Welcome back to" : "You are in:"} ${completed.communityName}.
17762
- Member key saved to ${path2} (expires ${completed.expiresAt.slice(0, 10)}).
17763
-
17764
- Next:
17765
- zanfia communities list
17766
- zanfia communities channels ${completed.communityId}
17767
- ` + (intro ? `
17768
- Introduce your agent:
17769
- ${intro}` : "") + `
17770
- Plug in your agent: ${AGENT_DOCS_URL}
18048
+ `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"}
17771
18049
  `
17772
18050
  );
17773
18051
  }
17774
- function registerJoinCommand(program2) {
17775
- program2.command("join <handle>").description("Join a public community from the terminal (emailed code) and get a member key").option("--email <email>", "email to join with (prompted when omitted)").option("--name <name>", "your display name (prompted when omitted)").option("--code <code>", "the 6-digit code from the email (prompted when omitted)").addHelpText(
17776
- "after",
17777
- `
17778
- Examples:
17779
- zanfia join crew-of-one
17780
- zanfia join crew-of-one --email me@example.com --name "Sam" --code 123456
17781
-
17782
- The key is a MEMBER key: it reaches the Community API for that creator's
17783
- communities only (read/post/comment/chat), never your own workspace. It is
17784
- saved to ~/.config/zanfia/credentials.json and expires in 90 days \u2014 run
17785
- join again to refresh it.
17786
- `
17787
- ).action(async (handle, options, command) => {
17788
- const globals = getGlobalFlags(command);
17789
- await runJoin(handle, { ...options, json: globals.json, verbose: globals.verbose });
17790
- });
18052
+ function printMetaEntries(entries) {
18053
+ printTable(
18054
+ entries.map((entry) => ({
18055
+ id: entry.id,
18056
+ name: entry.name,
18057
+ pixelId: entry.pixelId,
18058
+ serverSide: entry.serverSide ? "yes" : "",
18059
+ default: entry.isWorkspaceDefault ? "yes" : "",
18060
+ disabled: entry.disabled ? "yes" : "",
18061
+ events: [
18062
+ entry.trackPurchase ? "purchase" : null,
18063
+ entry.trackInitCheckout ? "initCheckout" : null,
18064
+ entry.trackAddToCart ? "addToCart" : null,
18065
+ entry.trackTrialStarted ? "trialStarted" : null
18066
+ ].filter(Boolean).join(",")
18067
+ })),
18068
+ ["id", "name", "pixelId", "serverSide", "default", "disabled", "events"]
18069
+ );
17791
18070
  }
17792
-
17793
- // src/commands/media/index.ts
17794
- import { existsSync, readFileSync as readFileSync15, writeFileSync as writeFileSync4 } from "fs";
17795
- import { readFile, stat as stat3 } from "fs/promises";
17796
- import { basename } from "path";
17797
-
17798
- // src/client/bunnyUpload.ts
17799
- import { createReadStream as createReadStream2 } from "fs";
18071
+ function printAccount(account, headline) {
18072
+ process.stdout.write(`${headline} ${account.client} ${account.url}
18073
+ `);
18074
+ process.stdout.write(` Lists: ${account.listsCount} Tags: ${account.tagsCount}
18075
+ `);
18076
+ }
18077
+ function buildEmailConnectInput(client, options) {
18078
+ if (client === "FreshMail") {
18079
+ throw new Error(
18080
+ "FreshMail can no longer be connected (provider retired); existing accounts can only be disconnected."
18081
+ );
18082
+ }
18083
+ const secret = readSecret("--api-key", options.apiKey, options.apiKeyStdin, options.apiKeyEnv);
18084
+ if (!secret) throw new Error("--api-key (or --api-key-stdin / --api-key-env VAR) is required.");
18085
+ const input = { client, url: options.url };
18086
+ switch (client) {
18087
+ case "ConvertKit":
18088
+ input.apiSecret = secret;
18089
+ break;
18090
+ case "Pipedrive":
18091
+ input.apiSecret = secret;
18092
+ if (!options.tagsField || !options.listsField) {
18093
+ throw new Error("Pipedrive needs --tags-field and --lists-field (person field names).");
18094
+ }
18095
+ input.tagsFieldName = options.tagsField;
18096
+ input.listsFieldName = options.listsField;
18097
+ break;
18098
+ case "MailChimp":
18099
+ input.apiKey = secret;
18100
+ if (!options.serverPrefix) throw new Error("MailChimp needs --server-prefix (e.g. us21).");
18101
+ input.serverPrefix = options.serverPrefix;
18102
+ break;
18103
+ default:
18104
+ input.apiKey = secret;
18105
+ }
18106
+ return input;
18107
+ }
18108
+ function buildMetaInput(options, existing) {
18109
+ const name = options.name ?? existing?.name;
18110
+ const pixelId = options.pixelId ?? existing?.pixelId;
18111
+ if (!name) throw new Error("--name is required.");
18112
+ if (!pixelId) throw new Error("--pixel-id is required.");
18113
+ const token = readSecret(
18114
+ "--access-token",
18115
+ options.accessToken,
18116
+ options.accessTokenStdin,
18117
+ options.accessTokenEnv
18118
+ );
18119
+ return {
18120
+ ...existing ? { id: existing.id } : {},
18121
+ name,
18122
+ pixelId,
18123
+ ...token !== void 0 ? { accessToken: token } : {},
18124
+ trackPurchase: parseOnOff2(options.trackPurchase, "--track-purchase") ?? existing?.trackPurchase ?? true,
18125
+ trackInitCheckout: parseOnOff2(options.trackInitCheckout, "--track-init-checkout") ?? existing?.trackInitCheckout ?? true,
18126
+ trackAddToCart: parseOnOff2(options.trackAddToCart, "--track-add-to-cart") ?? existing?.trackAddToCart ?? true,
18127
+ trackTrialStarted: parseOnOff2(options.trackTrialStarted, "--track-trial-started") ?? existing?.trackTrialStarted ?? false
18128
+ };
18129
+ }
18130
+ function addMetaOptions(command) {
18131
+ return command.option("--name <name>", "creator-facing label").option("--pixel-id <id>", "Meta pixel / dataset id").option(
18132
+ "--access-token <token>",
18133
+ "Conversions API access token (prefer --access-token-stdin / -env)"
18134
+ ).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)");
18135
+ }
18136
+ function registerIntegrationConnectionCommands(integrations) {
18137
+ integrations.command("status").description(
18138
+ "What is connected: email clients, Meta pixel / Conversions API entries, invoicing, payment methods (needs integrations:read)"
18139
+ ).action(async (_options, command) => {
18140
+ const flags = getGlobalFlags(command);
18141
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18142
+ const overview = await client.getIntegrationsOverview();
18143
+ emit(flags, overview, () => printOverview(overview));
18144
+ });
18145
+ const emailClients = integrations.command("email-clients").description(
18146
+ "Email-marketing accounts: ActiveCampaign, Kit (ConvertKit), MailerLite, MailChimp, GetResponse, Pipedrive. Keys are verified against the provider before saving and never printed."
18147
+ );
18148
+ emailClients.command("connect <client>").description(
18149
+ "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."
18150
+ ).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) => {
18151
+ const flags = getGlobalFlags(command);
18152
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18153
+ const input = buildEmailConnectInput(parseEmailClient(clientArg), options);
18154
+ const account = await client.connectEmailClient(input);
18155
+ emit(flags, account, () => printAccount(account, "Connected"));
18156
+ });
18157
+ emailClients.command("disconnect <client>").description(
18158
+ "Disconnect an account by url (irreversible, the stored key is deleted). Refused while a live newsletter / automation still uses it. Requires --confirm."
18159
+ ).requiredOption("--url <url>", "account url from `integrations status`").option("--confirm", "required \u2014 acknowledges the key is deleted").action(
18160
+ async (clientArg, options, command) => {
18161
+ requireConfirm7(options.confirm, `disconnect ${clientArg} ${options.url}`);
18162
+ const flags = getGlobalFlags(command);
18163
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18164
+ const parsed = parseEmailClient(clientArg);
18165
+ const result = await client.disconnectEmailClient(parsed, options.url);
18166
+ emit(flags, { ...result, client: parsed, url: options.url }, () => {
18167
+ process.stdout.write(`Disconnected ${parsed} ${options.url}
18168
+ `);
18169
+ });
18170
+ }
18171
+ );
18172
+ emailClients.command("refresh <client>").description(
18173
+ "Re-fetch the lists + tags of a connected account with the stored key (not Pipedrive)"
18174
+ ).requiredOption("--url <url>", "account url from `integrations status`").option("--server-prefix <prefix>", "MailChimp data-center prefix").action(
18175
+ async (clientArg, options, command) => {
18176
+ const flags = getGlobalFlags(command);
18177
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18178
+ const parsed = parseEmailClient(clientArg);
18179
+ if (parsed === "Pipedrive" || parsed === "FreshMail") {
18180
+ throw new Error(`${parsed} lists cannot be refreshed from the CLI.`);
18181
+ }
18182
+ const account = await client.refreshEmailClientLists({
18183
+ client: parsed,
18184
+ url: options.url,
18185
+ ...options.serverPrefix ? { serverPrefix: options.serverPrefix } : {}
18186
+ });
18187
+ emit(flags, account, () => printAccount(account, "Refreshed"));
18188
+ }
18189
+ );
18190
+ const meta = integrations.command("meta-capi").description(
18191
+ "Manual Meta pixel + Conversions API entries (the connect-with-Facebook OAuth path stays in the dashboard)"
18192
+ );
18193
+ meta.command("list").description("Meta pixel / Conversions API entries").action(async (_options, command) => {
18194
+ const flags = getGlobalFlags(command);
18195
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18196
+ const overview = await client.getIntegrationsOverview();
18197
+ emit(flags, overview.metaConversionApi, () => printMetaEntries(overview.metaConversionApi));
18198
+ });
18199
+ addMetaOptions(
18200
+ meta.command("add").description(
18201
+ "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."
18202
+ )
18203
+ ).action(async (options, command) => {
18204
+ const flags = getGlobalFlags(command);
18205
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18206
+ const entry = await client.connectMetaConversionApi(buildMetaInput(options, null));
18207
+ emit(flags, entry, () => printMetaEntries([entry]));
18208
+ });
18209
+ addMetaOptions(
18210
+ meta.command("update <integrationId>").description("Edit an entry \u2014 only the flags you pass change; an omitted token stays stored")
18211
+ ).action(async (integrationId, options, command) => {
18212
+ const flags = getGlobalFlags(command);
18213
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18214
+ const overview = await client.getIntegrationsOverview();
18215
+ const existing = overview.metaConversionApi.find((entry2) => entry2.id === integrationId);
18216
+ if (!existing) {
18217
+ throw new Error(
18218
+ `No Meta Conversions API entry with id ${integrationId}. Run \`zanfia integrations meta-capi list\`.`
18219
+ );
18220
+ }
18221
+ const entry = await client.connectMetaConversionApi(buildMetaInput(options, existing));
18222
+ emit(flags, entry, () => printMetaEntries([entry]));
18223
+ });
18224
+ 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) => {
18225
+ requireConfirm7(options.confirm, `remove Meta entry ${integrationId}`);
18226
+ const flags = getGlobalFlags(command);
18227
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18228
+ const result = await client.disconnectMetaConversionApi(integrationId);
18229
+ emit(flags, { ...result, integrationId }, () => {
18230
+ process.stdout.write(`Removed Meta entry ${integrationId}
18231
+ `);
18232
+ });
18233
+ });
18234
+ const manual = integrations.command("manual-payment").description(
18235
+ "The manual / wire-transfer payment method (buyers pay by transfer, you approve the order)"
18236
+ );
18237
+ 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(
18238
+ async (options, command) => {
18239
+ const instructions = options.instructions ?? (options.instructionsFile ? readFileSync15(options.instructionsFile, "utf8").trim() : void 0);
18240
+ if (!instructions) throw new Error("--instructions or --instructions-file is required.");
18241
+ const flags = getGlobalFlags(command);
18242
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18243
+ const method = await client.configureManualPayment({ name: options.name, instructions });
18244
+ emit(flags, method, () => {
18245
+ process.stdout.write(`Manual payment: ${method.name}
18246
+ ${method.instructions}
18247
+ `);
18248
+ });
18249
+ }
18250
+ );
18251
+ 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) => {
18252
+ requireConfirm7(options.confirm, "remove the manual payment method");
18253
+ const flags = getGlobalFlags(command);
18254
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18255
+ const result = await client.disconnectManualPayment();
18256
+ emit(flags, result, () => {
18257
+ process.stdout.write("Manual payment method removed\n");
18258
+ });
18259
+ });
18260
+ }
18261
+
18262
+ // src/commands/integrations/providers.ts
18263
+ import { readFileSync as readFileSync16 } from "fs";
18264
+ var INVOICES_ALIASES = {
18265
+ fakturownia: "fakturownia",
18266
+ ifirma: "ifirma",
18267
+ infakt: "inFakt",
18268
+ wfirma: "wFirmaV2",
18269
+ wfirmav2: "wFirmaV2",
18270
+ ing: "ingKsiegowosc",
18271
+ ingksiegowosc: "ingKsiegowosc",
18272
+ stripe: "stripe"
18273
+ };
18274
+ function parseInvoicesClient(value) {
18275
+ const match = INVOICES_ALIASES[value.trim().toLowerCase()];
18276
+ if (!match) {
18277
+ throw new Error(
18278
+ `Unknown invoicing client: ${value} (expected ${INVOICES_CLIENTS.join(" | ")})`
18279
+ );
18280
+ }
18281
+ return match;
18282
+ }
18283
+ function parseGateway(value) {
18284
+ const normalized = value.trim().toLowerCase().replace("p24", "przelewy24");
18285
+ const match = CREDENTIAL_PAYMENT_GATEWAYS.find((gateway) => gateway === normalized);
18286
+ if (!match) {
18287
+ throw new Error(
18288
+ `Unknown gateway: ${value} (expected ${CREDENTIAL_PAYMENT_GATEWAYS.join(" | ")}; Stripe connects in the dashboard, the transfer method is \`integrations manual-payment\`)`
18289
+ );
18290
+ }
18291
+ return match;
18292
+ }
18293
+ function parseEnum(value, allowed, flag) {
18294
+ const match = allowed.find((candidate) => candidate === value);
18295
+ if (!match) throw new Error(`Invalid ${flag}: ${value} (expected ${allowed.join(" | ")})`);
18296
+ return match;
18297
+ }
18298
+ function requireConfirm8(confirm, what) {
18299
+ if (!confirm) {
18300
+ throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
18301
+ }
18302
+ }
18303
+ function readSecret2(label, value, fromStdin, fromEnv) {
18304
+ const sources = [value !== void 0, fromStdin === true, fromEnv !== void 0].filter(Boolean);
18305
+ if (sources.length > 1) {
18306
+ throw new Error(`Pass ${label} through exactly one of the value, -stdin or -env forms.`);
18307
+ }
18308
+ if (fromStdin) {
18309
+ const raw = readFileSync16(0, "utf8").trim();
18310
+ if (!raw) throw new Error(`${label}: nothing was read from stdin.`);
18311
+ return raw;
18312
+ }
18313
+ if (fromEnv !== void 0) {
18314
+ const raw = process.env[fromEnv]?.trim();
18315
+ if (!raw) throw new Error(`${label}: environment variable ${fromEnv} is empty or unset.`);
18316
+ return raw;
18317
+ }
18318
+ return value;
18319
+ }
18320
+ function printPayments(payments) {
18321
+ const on = (value) => value ? "on" : "off";
18322
+ process.stdout.write(
18323
+ `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"}
18324
+ `
18325
+ );
18326
+ }
18327
+ function buildInvoicesInput(client, options) {
18328
+ const secret = readSecret2("--api-key", options.apiKey, options.apiKeyStdin, options.apiKeyEnv);
18329
+ const input = { client };
18330
+ if (options.id) input.id = options.id;
18331
+ if (options.name) input.accountName = options.name;
18332
+ if (options.url) input.url = options.url;
18333
+ if (options.departmentId) input.departmentId = options.departmentId;
18334
+ switch (client) {
18335
+ case "wFirmaV2":
18336
+ if (!options.accessKey || !options.secretKey || !options.companyId) {
18337
+ throw new Error("wFirma needs --access-key, --secret-key and --company-id.");
18338
+ }
18339
+ input.accessKey = options.accessKey;
18340
+ input.secretKey = options.secretKey;
18341
+ input.companyId = options.companyId;
18342
+ break;
18343
+ case "stripe":
18344
+ break;
18345
+ case "ifirma":
18346
+ if (secret) input.apiKey = secret;
18347
+ if (options.accountApiKey) input.accountApiKey = options.accountApiKey;
18348
+ break;
18349
+ case "ingKsiegowosc":
18350
+ if (secret) input.apiKey = secret;
18351
+ if (options.bankAccount) input.bankAccountNumber = options.bankAccount;
18352
+ break;
18353
+ default:
18354
+ if (secret) input.apiKey = secret;
18355
+ }
18356
+ if (client !== "stripe" && !input.url) throw new Error("--url is required.");
18357
+ return input;
18358
+ }
18359
+ function resolveSecret(value, fromEnv, flag) {
18360
+ if (value === void 0 || !fromEnv) return value;
18361
+ const raw = process.env[value]?.trim();
18362
+ if (!raw) throw new Error(`${flag}: environment variable ${value} is empty or unset.`);
18363
+ return raw;
18364
+ }
18365
+ function buildGatewayInput(gateway, o) {
18366
+ const need = (value, flag) => {
18367
+ if (!value) throw new Error(`${flag} is required for ${gateway}.`);
18368
+ return value;
18369
+ };
18370
+ const secret = (value, flag) => need(resolveSecret(value, o.secretsEnv, flag), flag);
18371
+ const input = { gateway, name: o.name };
18372
+ switch (gateway) {
18373
+ case "payu":
18374
+ input.posId = need(o.posId, "--pos-id");
18375
+ input.clientId = need(o.clientId, "--client-id");
18376
+ input.clientSecret = secret(o.clientSecret, "--client-secret");
18377
+ input.clientSecondKey = secret(o.secondKey, "--second-key");
18378
+ break;
18379
+ case "tpay":
18380
+ input.posId = need(o.posId, "--pos-id");
18381
+ input.clientId = need(o.clientId, "--client-id");
18382
+ input.clientSecret = secret(o.clientSecret, "--client-secret");
18383
+ input.safetyKey = secret(o.safetyKey, "--safety-key");
18384
+ break;
18385
+ case "przelewy24":
18386
+ input.merchantId = need(o.merchantId, "--merchant-id");
18387
+ input.posId = need(o.posId, "--pos-id");
18388
+ input.apiKey = secret(o.apiKey, "--api-key");
18389
+ input.crcKey = secret(o.crcKey, "--crc-key");
18390
+ break;
18391
+ case "paynow":
18392
+ input.apiKey = secret(o.apiKey, "--api-key");
18393
+ input.signatureKey = secret(o.signatureKey, "--signature-key");
18394
+ break;
18395
+ case "paypal":
18396
+ input.clientId = need(o.clientId, "--client-id");
18397
+ input.clientSecret = secret(o.clientSecret, "--client-secret");
18398
+ break;
18399
+ case "dodo":
18400
+ input.apiKey = secret(o.apiKey, "--api-key");
18401
+ break;
18402
+ }
18403
+ return input;
18404
+ }
18405
+ var TWILIO_REGIONS = ["us1", "ie1", "au1"];
18406
+ var TWILIO_AUTH_KINDS = ["authToken", "apiKey"];
18407
+ var TWILIO_SENDER_KINDS = [
18408
+ "phoneNumber",
18409
+ "messagingService"
18410
+ ];
18411
+ var SMSAPI_REGIONS = ["pl", "com"];
18412
+ var SMS_PROVIDERS = ["twilio", "smsapi"];
18413
+ function registerIntegrationProviderCommands(integrations) {
18414
+ const invoices = integrations.command("invoices").description(
18415
+ "Invoicing accounts: fakturownia, ifirma, infakt, wfirma (v2), ing (ING Ksi\u0119gowo\u015B\u0107), stripe. Keys are verified with the provider and never printed."
18416
+ );
18417
+ 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) => {
18418
+ const flags = getGlobalFlags(command);
18419
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18420
+ const account = await client.connectInvoicesClient(
18421
+ buildInvoicesInput(parseInvoicesClient(clientArg), options)
18422
+ );
18423
+ emit(flags, account, () => {
18424
+ process.stdout.write(`Connected ${account.client} ${account.id} (${account.url})
18425
+ `);
18426
+ });
18427
+ });
18428
+ invoices.command("disconnect <client> <accountId>").description("Disconnect an invoicing account (irreversible). Requires --confirm.").option("--confirm", "required \u2014 acknowledges the stored key is deleted").action(
18429
+ async (clientArg, accountId, options, command) => {
18430
+ requireConfirm8(options.confirm, `disconnect ${clientArg} ${accountId}`);
18431
+ const flags = getGlobalFlags(command);
18432
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18433
+ const parsed = parseInvoicesClient(clientArg);
18434
+ const result = await client.disconnectInvoicesClient(parsed, accountId);
18435
+ emit(flags, { ...result, client: parsed, id: accountId }, () => {
18436
+ process.stdout.write(`Disconnected ${parsed} ${accountId}
18437
+ `);
18438
+ });
18439
+ }
18440
+ );
18441
+ const payments = integrations.command("payments").description(
18442
+ "Credential payment gateways: payu, tpay, przelewy24 (p24), paynow, paypal, dodo. Stripe connects in the dashboard (OAuth); the transfer method is `integrations manual-payment`."
18443
+ );
18444
+ payments.command("connect <gateway>").description(
18445
+ "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."
18446
+ ).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) => {
18447
+ const flags = getGlobalFlags(command);
18448
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18449
+ const status = await client.connectPaymentGateway(
18450
+ buildGatewayInput(parseGateway(gatewayArg), options)
18451
+ );
18452
+ emit(flags, status, () => printPayments(status));
18453
+ });
18454
+ 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) => {
18455
+ requireConfirm8(options.confirm, `disconnect ${gatewayArg}`);
18456
+ const flags = getGlobalFlags(command);
18457
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18458
+ const status = await client.disconnectPaymentGateway(parseGateway(gatewayArg));
18459
+ emit(flags, status, () => printPayments(status));
18460
+ });
18461
+ payments.command("tpay-blik <on|off>").description("Turn Tpay recurring BLIK (BLIK subscriptions) on or off").action(async (value, _options, command) => {
18462
+ const enabled = parseEnum(value, ["on", "off"], "tpay-blik") === "on";
18463
+ const flags = getGlobalFlags(command);
18464
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18465
+ const result = await client.setTpayRecurringBlik(enabled);
18466
+ emit(flags, { ...result, enabled }, () => {
18467
+ process.stdout.write(`Tpay recurring BLIK: ${enabled ? "on" : "off"}
18468
+ `);
18469
+ });
18470
+ });
18471
+ const sms = integrations.command("sms").description("SMS providers (Twilio, SMSAPI) and which one campaigns send through");
18472
+ const twilio = sms.command("twilio").description("Twilio account");
18473
+ 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(
18474
+ async (options, command) => {
18475
+ const token = readSecret2(
18476
+ "--auth-token",
18477
+ options.authToken,
18478
+ options.authTokenStdin,
18479
+ options.authTokenEnv
18480
+ );
18481
+ if (!token)
18482
+ throw new Error(
18483
+ "--auth-token (or --auth-token-stdin / --auth-token-env VAR) is required."
18484
+ );
18485
+ const input = {
18486
+ accountSid: options.accountSid,
18487
+ authToken: token,
18488
+ senderKind: parseEnum(options.senderKind, TWILIO_SENDER_KINDS, "--sender-kind"),
18489
+ sender: options.sender
18490
+ };
18491
+ if (options.region) input.region = parseEnum(options.region, TWILIO_REGIONS, "--region");
18492
+ if (options.authKind)
18493
+ input.authKind = parseEnum(options.authKind, TWILIO_AUTH_KINDS, "--auth-kind");
18494
+ if (options.apiKeySid) input.apiKeySid = options.apiKeySid;
18495
+ const flags = getGlobalFlags(command);
18496
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18497
+ const result = await client.connectTwilio(input);
18498
+ emit(flags, result, () => {
18499
+ process.stdout.write(
18500
+ `Connected Twilio ${result.accountSid} (${result.region}) sender ${result.sender}
18501
+ `
18502
+ );
18503
+ });
18504
+ }
18505
+ );
18506
+ twilio.command("disconnect").description("Disconnect Twilio (secret deleted). Requires --confirm.").option("--confirm", "required").action(async (options, command) => {
18507
+ requireConfirm8(options.confirm, "disconnect Twilio");
18508
+ const flags = getGlobalFlags(command);
18509
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18510
+ const result = await client.disconnectTwilio();
18511
+ emit(flags, result, () => {
18512
+ process.stdout.write("Twilio disconnected\n");
18513
+ });
18514
+ });
18515
+ const smsapi = sms.command("smsapi").description("SMSAPI account (smsapi.pl / smsapi.com)");
18516
+ smsapi.command("connect").description(
18517
+ "Connect (or replace) the SMSAPI account \u2014 the token is verified with SMSAPI first"
18518
+ ).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(
18519
+ async (options, command) => {
18520
+ const token = readSecret2(
18521
+ "--api-token",
18522
+ options.apiToken,
18523
+ options.apiTokenStdin,
18524
+ options.apiTokenEnv
18525
+ );
18526
+ if (!token)
18527
+ throw new Error("--api-token (or --api-token-stdin / --api-token-env VAR) is required.");
18528
+ const flags = getGlobalFlags(command);
18529
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18530
+ const result = await client.connectSmsApi({
18531
+ region: options.region ? parseEnum(options.region, SMSAPI_REGIONS, "--region") : "pl",
18532
+ apiToken: token,
18533
+ sender: options.sender
18534
+ });
18535
+ emit(flags, result, () => {
18536
+ process.stdout.write(
18537
+ `Connected SMSAPI (${result.region}) sender ${result.sender}${result.points !== null ? `, ${result.points} points` : ""}
18538
+ `
18539
+ );
18540
+ });
18541
+ }
18542
+ );
18543
+ smsapi.command("disconnect").description("Disconnect SMSAPI (token deleted). Requires --confirm.").option("--confirm", "required").action(async (options, command) => {
18544
+ requireConfirm8(options.confirm, "disconnect SMSAPI");
18545
+ const flags = getGlobalFlags(command);
18546
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18547
+ const result = await client.disconnectSmsApi();
18548
+ emit(flags, result, () => {
18549
+ process.stdout.write("SMSAPI disconnected\n");
18550
+ });
18551
+ });
18552
+ smsapi.command("refresh-senders").description("Re-fetch the approved SMSAPI sender names").action(async (_options, command) => {
18553
+ const flags = getGlobalFlags(command);
18554
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18555
+ const result = await client.refreshSmsApiSenders();
18556
+ emit(flags, result, () => {
18557
+ process.stdout.write(
18558
+ `Senders: ${result.senders.join(", ") || "(none)"}
18559
+ Default: ${result.sender}
18560
+ `
18561
+ );
18562
+ });
18563
+ });
18564
+ smsapi.command("default-sender <sender>").description("Pick which approved SMSAPI sender campaigns send from").action(async (sender, _options, command) => {
18565
+ const flags = getGlobalFlags(command);
18566
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18567
+ const result = await client.setDefaultSmsApiSender(sender);
18568
+ emit(flags, result, () => {
18569
+ process.stdout.write(`Default SMSAPI sender: ${result.sender}
18570
+ `);
18571
+ });
18572
+ });
18573
+ sms.command("default-provider <twilio|smsapi>").description("Choose which connected SMS provider campaigns and automations send through").action(async (value, _options, command) => {
18574
+ const provider = parseEnum(value, SMS_PROVIDERS, "default-provider");
18575
+ const flags = getGlobalFlags(command);
18576
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18577
+ const result = await client.setDefaultSmsProvider(provider);
18578
+ emit(flags, result, () => {
18579
+ process.stdout.write(`Default SMS provider: ${result.defaultProvider}
18580
+ `);
18581
+ });
18582
+ });
18583
+ }
18584
+
18585
+ // src/commands/integrations/index.ts
18586
+ var CLIENT_ALIASES = {
18587
+ gtm: "googleTagManager",
18588
+ ga4: "googleAnalytics",
18589
+ ga: "googleAnalytics",
18590
+ clarity: "microsoftClarity",
18591
+ twitter: "x"
18592
+ };
18593
+ var NONE = "none";
18594
+ function parseClient(value) {
18595
+ const alias = CLIENT_ALIASES[value.trim().toLowerCase()];
18596
+ if (alias) return alias;
18597
+ const match = MANAGED_CONVERSION_TRACKING_CLIENTS.find(
18598
+ (client) => client.toLowerCase() === value.trim().toLowerCase()
18599
+ );
18600
+ if (!match) {
18601
+ throw new Error(
18602
+ `Unknown platform: ${value} (expected ${MANAGED_CONVERSION_TRACKING_CLIENTS.join(" | ")}; aliases: ${Object.keys(CLIENT_ALIASES).join(", ")}). Meta pixel / CAPI is dashboard-only.`
18603
+ );
18604
+ }
18605
+ return match;
18606
+ }
18607
+ function parseOnOff3(value, flag) {
18608
+ if (value === void 0) return void 0;
18609
+ if (value === "on") return true;
18610
+ if (value === "off") return false;
18611
+ throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
18612
+ }
18613
+ function textOrNull(value) {
18614
+ if (value === void 0) return void 0;
18615
+ return value.trim().toLowerCase() === NONE ? null : value;
18616
+ }
18617
+ function requireConfirm9(confirm, what) {
18618
+ if (!confirm) {
18619
+ throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
18620
+ }
18621
+ }
18622
+ var TRACK_FLAG_DEFAULTS = {
18623
+ trackPurchase: true,
18624
+ trackInitCheckout: true,
18625
+ trackAddToCart: true,
18626
+ trackTrialStarted: false
18627
+ };
18628
+ var REQUIRED_ID_FLAG = {
18629
+ googleAnalytics: "measurementId",
18630
+ googleTagManager: "containerId",
18631
+ tiktok: "pixelId",
18632
+ x: "pixelId",
18633
+ linkedin: "partnerId",
18634
+ microsoftClarity: "projectId",
18635
+ hotjar: "siteId"
18636
+ };
18637
+ var FLAG_NAME = {
18638
+ name: "--name",
18639
+ containerId: "--container-id",
18640
+ measurementId: "--measurement-id",
18641
+ apiSecret: "--api-secret",
18642
+ pixelId: "--pixel-id",
18643
+ accessToken: "--access-token",
18644
+ partnerId: "--partner-id",
18645
+ projectId: "--project-id",
18646
+ siteId: "--site-id",
18647
+ purchaseTag: "--purchase-tag",
18648
+ initCheckoutTag: "--init-checkout-tag",
18649
+ addToCartTag: "--add-to-cart-tag",
18650
+ trialStartedTag: "--trial-started-tag",
18651
+ trackPurchase: "--track-purchase",
18652
+ trackInitCheckout: "--track-init-checkout",
18653
+ trackAddToCart: "--track-add-to-cart",
18654
+ trackTrialStarted: "--track-trial-started"
18655
+ };
18656
+ var hasEventFlags = (client) => client === "googleAnalytics" || client === "tiktok" || client === "x" || client === "linkedin";
18657
+ function buildUpsertInput(client, options, existing) {
18658
+ const name = options.name ?? existing?.name;
18659
+ if (!name) throw new Error("--name is required when adding an integration.");
18660
+ const input = { client, name };
18661
+ if (existing) input.integrationId = existing.id;
18662
+ const existingFields = existing ? { ...existing } : {};
18663
+ const pick = (key, provided) => {
18664
+ if (provided !== void 0) return provided;
18665
+ const value = existingFields[key];
18666
+ return typeof value === "string" ? value : void 0;
18667
+ };
18668
+ switch (client) {
18669
+ case "googleTagManager":
18670
+ input.containerId = pick("containerId", options.containerId);
18671
+ break;
18672
+ case "googleAnalytics":
18673
+ input.measurementId = pick("measurementId", options.measurementId);
18674
+ if (options.apiSecret !== void 0) input.apiSecret = textOrNull(options.apiSecret);
18675
+ break;
18676
+ case "tiktok":
18677
+ input.pixelId = pick("pixelId", options.pixelId);
18678
+ if (options.accessToken !== void 0) input.accessToken = textOrNull(options.accessToken);
18679
+ break;
18680
+ case "x":
18681
+ input.pixelId = pick("pixelId", options.pixelId);
18682
+ if (options.accessToken !== void 0) input.accessToken = textOrNull(options.accessToken);
18683
+ input.purchaseEventTag = textOrNull(options.purchaseTag) ?? pick("purchaseEventTag", void 0) ?? null;
18684
+ input.initCheckoutEventTag = textOrNull(options.initCheckoutTag) ?? pick("initCheckoutEventTag", void 0) ?? null;
18685
+ input.addToCartEventTag = textOrNull(options.addToCartTag) ?? pick("addToCartEventTag", void 0) ?? null;
18686
+ input.trialStartedEventTag = textOrNull(options.trialStartedTag) ?? pick("trialStartedEventTag", void 0) ?? null;
18687
+ break;
18688
+ case "linkedin":
18689
+ input.partnerId = pick("partnerId", options.partnerId);
18690
+ if (options.accessToken !== void 0) input.accessToken = textOrNull(options.accessToken);
18691
+ input.purchaseConversionUrn = textOrNull(options.purchaseTag) ?? pick("purchaseConversionUrn", void 0) ?? null;
18692
+ input.initCheckoutConversionUrn = textOrNull(options.initCheckoutTag) ?? pick("initCheckoutConversionUrn", void 0) ?? null;
18693
+ input.addToCartConversionUrn = textOrNull(options.addToCartTag) ?? pick("addToCartConversionUrn", void 0) ?? null;
18694
+ input.trialStartedConversionUrn = textOrNull(options.trialStartedTag) ?? pick("trialStartedConversionUrn", void 0) ?? null;
18695
+ break;
18696
+ case "microsoftClarity":
18697
+ input.projectId = pick("projectId", options.projectId);
18698
+ break;
18699
+ case "hotjar":
18700
+ input.siteId = pick("siteId", options.siteId);
18701
+ break;
18702
+ }
18703
+ const requiredKey = REQUIRED_ID_FLAG[client];
18704
+ if (input[requiredKey] === void 0) {
18705
+ throw new Error(`${FLAG_NAME[requiredKey]} is required for ${client}.`);
18706
+ }
18707
+ if (hasEventFlags(client)) {
18708
+ const flags = existing && "trackPurchase" in existing ? existing : TRACK_FLAG_DEFAULTS;
18709
+ input.trackPurchase = parseOnOff3(options.trackPurchase, FLAG_NAME.trackPurchase) ?? flags.trackPurchase;
18710
+ input.trackInitCheckout = parseOnOff3(options.trackInitCheckout, FLAG_NAME.trackInitCheckout) ?? flags.trackInitCheckout;
18711
+ input.trackAddToCart = parseOnOff3(options.trackAddToCart, FLAG_NAME.trackAddToCart) ?? flags.trackAddToCart;
18712
+ input.trackTrialStarted = parseOnOff3(options.trackTrialStarted, FLAG_NAME.trackTrialStarted) ?? flags.trackTrialStarted;
18713
+ }
18714
+ return input;
18715
+ }
18716
+ function platformIdOf(integration) {
18717
+ switch (integration.client) {
18718
+ case "googleAnalytics":
18719
+ return integration.measurementId;
18720
+ case "googleTagManager":
18721
+ return integration.containerId;
18722
+ case "tiktok":
18723
+ case "x":
18724
+ return integration.pixelId;
18725
+ case "linkedin":
18726
+ return integration.partnerId;
18727
+ case "microsoftClarity":
18728
+ return integration.projectId;
18729
+ case "hotjar":
18730
+ return integration.siteId;
18731
+ }
18732
+ }
18733
+ function serverSideOf(integration) {
18734
+ if ("apiSecretPath" in integration) return integration.apiSecretPath ? "yes" : "";
18735
+ return "n/a";
18736
+ }
18737
+ function printIntegrations(integrations) {
18738
+ printTable(
18739
+ integrations.map((integration) => ({
18740
+ id: integration.id,
18741
+ platform: integration.client,
18742
+ name: integration.name,
18743
+ platformId: platformIdOf(integration),
18744
+ serverSide: serverSideOf(integration),
18745
+ default: integration.isWorkspaceDefault ? "yes" : "",
18746
+ updatedAt: integration.updatedAt
18747
+ })),
18748
+ ["id", "platform", "name", "platformId", "serverSide", "default", "updatedAt"]
18749
+ );
18750
+ }
18751
+ function printIntegration(integration, headline) {
18752
+ process.stdout.write(`${headline} ${integration.id} (${integration.client})
18753
+ `);
18754
+ process.stdout.write(` Name: ${integration.name}
18755
+ `);
18756
+ process.stdout.write(` Platform id: ${platformIdOf(integration)}
18757
+ `);
18758
+ process.stdout.write(` Workspace default: ${integration.isWorkspaceDefault ? "yes" : "no"}
18759
+ `);
18760
+ if ("trackPurchase" in integration) {
18761
+ process.stdout.write(
18762
+ ` Events: purchase=${integration.trackPurchase ? "on" : "off"} initCheckout=${integration.trackInitCheckout ? "on" : "off"} addToCart=${integration.trackAddToCart ? "on" : "off"} trialStarted=${integration.trackTrialStarted ? "on" : "off"}
18763
+ `
18764
+ );
18765
+ }
18766
+ if ("apiSecretPath" in integration) {
18767
+ process.stdout.write(` Server-side events: ${integration.apiSecretPath ? "on" : "off"}
18768
+ `);
18769
+ }
18770
+ }
18771
+ function addPlatformOptions(command) {
18772
+ 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(
18773
+ "--api-secret <secret|none>",
18774
+ "googleAnalytics: Measurement Protocol API secret for server-side events (validated against GA4); none = client-side only"
18775
+ ).option("--pixel-id <id>", "tiktok / x: pixel id").option(
18776
+ "--access-token <token|none>",
18777
+ "tiktok / x / linkedin: events API access token for server-side events (TikTok/LinkedIn tokens are validated); none = client-side only"
18778
+ ).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(
18779
+ "--purchase-tag <tag|none>",
18780
+ "x: purchase event tag / linkedin: purchase conversion URN"
18781
+ ).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(
18782
+ "--track-init-checkout <on|off>",
18783
+ "GA4 / TikTok / X / LinkedIn: send InitiateCheckout (default on)"
18784
+ ).option(
18785
+ "--track-add-to-cart <on|off>",
18786
+ "GA4 / TikTok / X / LinkedIn: send AddToCart (default on)"
18787
+ ).option(
18788
+ "--track-trial-started <on|off>",
18789
+ "GA4 / TikTok / X / LinkedIn: send TrialStarted (default off)"
18790
+ );
18791
+ }
18792
+ function registerIntegrationsCommands(program2) {
18793
+ const integrations = program2.command("integrations").description(
18794
+ "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`."
18795
+ );
18796
+ registerIntegrationConnectionCommands(integrations);
18797
+ registerIntegrationProviderCommands(integrations);
18798
+ integrations.command("list").description("List the managed conversion-tracking integrations and the workspace defaults").option(
18799
+ "--client <platform>",
18800
+ "only one platform (e.g. gtm, ga4, tiktok, x, linkedin, clarity, hotjar)"
18801
+ ).action(async (options, command) => {
18802
+ const flags = getGlobalFlags(command);
18803
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18804
+ const result = await client.listConversionTrackingIntegrations();
18805
+ const filter = options.client !== void 0 ? parseClient(options.client) : void 0;
18806
+ const entries = filter ? result.integrations.filter((integration) => integration.client === filter) : result.integrations;
18807
+ emit(flags, { ...result, integrations: entries }, () => {
18808
+ printIntegrations(entries);
18809
+ process.stdout.write(
18810
+ `Workspace defaults: ${result.defaultIntegrationIds.length > 0 ? result.defaultIntegrationIds.join(", ") : "(none)"}
18811
+ `
18812
+ );
18813
+ });
18814
+ });
18815
+ addPlatformOptions(
18816
+ integrations.command("add <platform>").description(
18817
+ "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"
18818
+ )
18819
+ ).action(async (platform2, options, command) => {
18820
+ const flags = getGlobalFlags(command);
18821
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18822
+ const input = buildUpsertInput(parseClient(platform2), options, null);
18823
+ const result = await client.upsertConversionTrackingIntegration(input);
18824
+ emit(flags, result, () => printIntegration(result, "Added"));
18825
+ });
18826
+ addPlatformOptions(
18827
+ integrations.command("update <platform> <integrationId>").description(
18828
+ "Edit an integration \u2014 only the flags you pass change; omitted secrets stay stored. Ids come from `integrations list`."
18829
+ )
18830
+ ).action(
18831
+ async (platform2, integrationId, options, command) => {
18832
+ const flags = getGlobalFlags(command);
18833
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18834
+ const parsedClient = parseClient(platform2);
18835
+ const current = await client.listConversionTrackingIntegrations();
18836
+ const existing = current.integrations.find(
18837
+ (integration) => integration.id === integrationId && integration.client === parsedClient
18838
+ );
18839
+ if (!existing) {
18840
+ throw new Error(
18841
+ `No ${parsedClient} integration with id ${integrationId}. Run \`zanfia integrations list\`.`
18842
+ );
18843
+ }
18844
+ const input = buildUpsertInput(parsedClient, options, existing);
18845
+ const result = await client.upsertConversionTrackingIntegration(input);
18846
+ emit(flags, result, () => printIntegration(result, "Updated"));
18847
+ }
18848
+ );
18849
+ integrations.command("remove <platform> <integrationId>").description(
18850
+ "Remove an integration (irreversible). Offers and the workspace defaults that still list the id simply stop tracking on that platform. Requires --confirm."
18851
+ ).option("--confirm", "required \u2014 acknowledges the integration is deleted").action(
18852
+ async (platform2, integrationId, options, command) => {
18853
+ requireConfirm9(options.confirm, `remove integration ${integrationId}`);
18854
+ const flags = getGlobalFlags(command);
18855
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18856
+ const result = await client.removeConversionTrackingIntegration(
18857
+ parseClient(platform2),
18858
+ integrationId
18859
+ );
18860
+ emit(flags, { ...result, integrationId }, () => {
18861
+ process.stdout.write(`Removed integration ${integrationId}
18862
+ `);
18863
+ });
18864
+ }
18865
+ );
18866
+ integrations.command("set-defaults <ids|none>").description(
18867
+ "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."
18868
+ ).action(async (ids, _options, command) => {
18869
+ const flags = getGlobalFlags(command);
18870
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18871
+ const integrationIds = ids.trim().toLowerCase() === NONE ? [] : ids.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
18872
+ const result = await client.setDefaultConversionTracking(integrationIds);
18873
+ emit(flags, result, () => {
18874
+ process.stdout.write(
18875
+ `Workspace defaults: ${result.integrationIds.length > 0 ? result.integrationIds.join(", ") : "(none)"}
18876
+ `
18877
+ );
18878
+ });
18879
+ });
18880
+ }
18881
+
18882
+ // src/commands/join/index.ts
18883
+ var MAX_CODE_ATTEMPTS2 = 5;
18884
+ var HANDLE_REGEX = /^[a-z0-9][a-z0-9-]{1,62}$/;
18885
+ var EMAIL_REGEX2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
18886
+ var AGENT_DOCS_URL = "https://zanfia.com/help/developer-cli/agents";
18887
+ function joinUrl(base, handle, step) {
18888
+ return `${base.replace(/\/$/, "")}/v1/join/${encodeURIComponent(handle)}/${step}`;
18889
+ }
18890
+ async function runJoin(handleInput, options) {
18891
+ const handle = handleInput.trim().toLowerCase();
18892
+ if (!HANDLE_REGEX.test(handle)) {
18893
+ throw new Error(
18894
+ "Community handle must be lowercase letters, digits and dashes (e.g. crew-of-one)."
18895
+ );
18896
+ }
18897
+ const communityApiUrl = resolveCommunityApiUrl();
18898
+ if (!communityApiUrl) {
18899
+ throw new Error(
18900
+ "No Community API URL configured. Set ZANFIA_COMMUNITY_API_URL (this build has no baked backend)."
18901
+ );
18902
+ }
18903
+ const verbose = options.verbose === true;
18904
+ const email = (options.email ?? await promptLine("Email: ")).trim().toLowerCase();
18905
+ if (!EMAIL_REGEX2.test(email)) {
18906
+ throw new Error("Please enter a valid email address.");
18907
+ }
18908
+ const started = await postJson2(
18909
+ "Join",
18910
+ joinUrl(communityApiUrl, handle, "start"),
18911
+ { email, lang: "en" },
18912
+ { verbose }
18913
+ );
18914
+ if (!options.json) {
18915
+ process.stdout.write(
18916
+ `Joining ${started.communityName} (${started.workspaceName}) \u2014 a 6-digit code was sent to ${email}.
18917
+ `
18918
+ );
18919
+ }
18920
+ const firstName = options.name ?? (options.json ? "" : await promptLine("Your name (optional): "));
18921
+ const hostname3 = safeHostname2();
18922
+ let completed = null;
18923
+ for (let attempt = 1; attempt <= MAX_CODE_ATTEMPTS2 && !completed; attempt += 1) {
18924
+ const code = options.code ?? (await promptLine("Code from the email (or 'r' to resend): ")).trim();
18925
+ if (code.toLowerCase() === "r") {
18926
+ await postJson2(
18927
+ "Resend",
18928
+ joinUrl(communityApiUrl, handle, "start"),
18929
+ { email, lang: "en" },
18930
+ { verbose }
18931
+ );
18932
+ process.stdout.write("Code re-sent.\n");
18933
+ attempt -= 1;
18934
+ continue;
18935
+ }
18936
+ try {
18937
+ completed = await postJson2(
18938
+ "Join",
18939
+ joinUrl(communityApiUrl, handle, "complete"),
18940
+ { email, code, firstName, hostname: hostname3 },
18941
+ { verbose }
18942
+ );
18943
+ } catch (err) {
18944
+ if (options.code || attempt === MAX_CODE_ATTEMPTS2) throw err;
18945
+ process.stdout.write(`${err instanceof Error ? err.message : String(err)}
18946
+ `);
18947
+ }
18948
+ }
18949
+ if (!completed) {
18950
+ throw new Error("Could not verify the code.");
18951
+ }
18952
+ const path2 = writeCredentials({
18953
+ apiKey: completed.token,
18954
+ apiUrl: communityApiUrl,
18955
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
18956
+ source: "oauth",
18957
+ expiresAt: completed.expiresAt,
18958
+ userEmail: completed.userEmail,
18959
+ workspaceId: completed.workspaceId,
18960
+ workspaceName: completed.workspaceName,
18961
+ kind: "member"
18962
+ });
18963
+ if (options.json) {
18964
+ process.stdout.write(
18965
+ `${JSON.stringify(
18966
+ {
18967
+ joined: true,
18968
+ alreadyMember: completed.alreadyMember,
18969
+ communityId: completed.communityId,
18970
+ communityName: completed.communityName,
18971
+ workspaceId: completed.workspaceId,
18972
+ workspaceName: completed.workspaceName,
18973
+ introChannelId: completed.introChannelId,
18974
+ userEmail: completed.userEmail,
18975
+ expiresAt: completed.expiresAt,
18976
+ credentialsPath: path2
18977
+ },
18978
+ null,
18979
+ 2
18980
+ )}
18981
+ `
18982
+ );
18983
+ return;
18984
+ }
18985
+ const intro = completed.introChannelId ? ` zanfia communities post create ${completed.communityId} ${completed.introChannelId} --content "Hi, I'm <your agent>. I ..."
18986
+ ` : "";
18987
+ process.stdout.write(
18988
+ `
18989
+ ${completed.alreadyMember ? "Welcome back to" : "You are in:"} ${completed.communityName}.
18990
+ Member key saved to ${path2} (expires ${completed.expiresAt.slice(0, 10)}).
18991
+
18992
+ Next:
18993
+ zanfia communities list
18994
+ zanfia communities channels ${completed.communityId}
18995
+ ` + (intro ? `
18996
+ Introduce your agent:
18997
+ ${intro}` : "") + `
18998
+ Plug in your agent: ${AGENT_DOCS_URL}
18999
+ `
19000
+ );
19001
+ }
19002
+ function registerJoinCommand(program2) {
19003
+ program2.command("join <handle>").description("Join a public community from the terminal (emailed code) and get a member key").option("--email <email>", "email to join with (prompted when omitted)").option("--name <name>", "your display name (prompted when omitted)").option("--code <code>", "the 6-digit code from the email (prompted when omitted)").addHelpText(
19004
+ "after",
19005
+ `
19006
+ Examples:
19007
+ zanfia join crew-of-one
19008
+ zanfia join crew-of-one --email me@example.com --name "Sam" --code 123456
19009
+
19010
+ The key is a MEMBER key: it reaches the Community API for that creator's
19011
+ communities only (read/post/comment/chat), never your own workspace. It is
19012
+ saved to ~/.config/zanfia/credentials.json and expires in 90 days \u2014 run
19013
+ join again to refresh it.
19014
+ `
19015
+ ).action(async (handle, options, command) => {
19016
+ const globals = getGlobalFlags(command);
19017
+ await runJoin(handle, { ...options, json: globals.json, verbose: globals.verbose });
19018
+ });
19019
+ }
19020
+
19021
+ // src/commands/media/index.ts
19022
+ import { existsSync, readFileSync as readFileSync17, writeFileSync as writeFileSync4 } from "fs";
19023
+ import { readFile, stat as stat3 } from "fs/promises";
19024
+ import { basename } from "path";
19025
+
19026
+ // src/client/bunnyUpload.ts
19027
+ import { createReadStream as createReadStream2 } from "fs";
17800
19028
  import { stat as stat2 } from "fs/promises";
17801
19029
 
17802
19030
  // ../../node_modules/tus-js-client/lib.esm/error.js
@@ -20909,16 +22137,61 @@ async function uploadVideoToBunny(params) {
20909
22137
  });
20910
22138
  }
20911
22139
 
22140
+ // src/utils/mime.ts
22141
+ var OCTET_STREAM_MIME = "application/octet-stream";
22142
+ function mimeFromName(name) {
22143
+ const n = name.toLowerCase();
22144
+ if (n.endsWith(".png")) return "image/png";
22145
+ if (n.endsWith(".jpg") || n.endsWith(".jpeg")) return "image/jpeg";
22146
+ if (n.endsWith(".webp")) return "image/webp";
22147
+ if (n.endsWith(".gif")) return "image/gif";
22148
+ if (n.endsWith(".svg")) return "image/svg+xml";
22149
+ if (n.endsWith(".avif")) return "image/avif";
22150
+ if (n.endsWith(".pdf")) return "application/pdf";
22151
+ if (n.endsWith(".zip")) return "application/zip";
22152
+ if (n.endsWith(".mp3")) return "audio/mpeg";
22153
+ if (n.endsWith(".mp4")) return "video/mp4";
22154
+ if (n.endsWith(".txt") || n.endsWith(".md")) return "text/plain";
22155
+ if (n.endsWith(".csv")) return "text/csv";
22156
+ if (n.endsWith(".json")) return "application/json";
22157
+ if (n.endsWith(".epub")) return "application/epub+zip";
22158
+ if (n.endsWith(".docx")) {
22159
+ return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
22160
+ }
22161
+ if (n.endsWith(".xlsx")) {
22162
+ return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
22163
+ }
22164
+ return null;
22165
+ }
22166
+ function mimeFromBytes(bytes) {
22167
+ const ascii = (start, text) => bytes.length >= start + text.length && bytes.toString("latin1", start, start + text.length) === text;
22168
+ if (bytes.length >= 8 && bytes[0] === 137 && ascii(1, "PNG\r\n\n")) return "image/png";
22169
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
22170
+ return "image/jpeg";
22171
+ }
22172
+ if (ascii(0, "GIF87a") || ascii(0, "GIF89a")) return "image/gif";
22173
+ if (ascii(0, "RIFF") && ascii(8, "WEBP")) return "image/webp";
22174
+ if (ascii(4, "ftypavif")) return "image/avif";
22175
+ if (ascii(0, "%PDF-")) return "application/pdf";
22176
+ if (bytes.toString("utf8", 0, Math.min(bytes.length, 1024)).toLowerCase().includes("<svg")) {
22177
+ return "image/svg+xml";
22178
+ }
22179
+ return null;
22180
+ }
22181
+ function inferMimeType(name, bytes) {
22182
+ return mimeFromName(name) ?? mimeFromBytes(bytes) ?? OCTET_STREAM_MIME;
22183
+ }
22184
+
20912
22185
  // src/commands/media/index.ts
20913
22186
  var DEFAULT_AI_IMAGE_ASPECT = "1:1" /* Square */;
20914
22187
  var DEFAULT_AI_IMAGE_QUALITY = "1K" /* Standard */;
20915
- function requireConfirm7(confirm, what) {
22188
+ function requireConfirm10(confirm, what) {
20916
22189
  if (!confirm) {
20917
22190
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
20918
22191
  }
20919
22192
  }
20920
22193
  function resolveTextOrFile(value) {
20921
- const text = existsSync(value) ? readFileSync15(value, "utf8") : value;
22194
+ const text = existsSync(value) ? readFileSync17(value, "utf8") : value;
20922
22195
  if (!text.trim()) throw new Error("The text (or the file it points to) is empty.");
20923
22196
  return text.trim();
20924
22197
  }
@@ -20974,32 +22247,6 @@ var TRANSCRIBE_STATUS_TEXT = {
20974
22247
  ["already_running" /* AlreadyRunning */]: "Transcription is already running",
20975
22248
  ["already_transcribed" /* AlreadyTranscribed */]: "Already transcribed \u2014 nothing queued (pass --force to re-transcribe)"
20976
22249
  };
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
22250
  function videoMimeFromName(name) {
21004
22251
  const n = name.toLowerCase();
21005
22252
  if (n.endsWith(".mov")) return "video/quicktime";
@@ -21022,7 +22269,7 @@ function registerMediaCommands(program2) {
21022
22269
  const name = options.name ?? basename(options.file);
21023
22270
  const input = {
21024
22271
  name,
21025
- mimeType: options.mime ?? mimeFromName(name) ?? mimeFromBytes(bytes) ?? "application/octet-stream",
22272
+ mimeType: options.mime ?? inferMimeType(name, bytes),
21026
22273
  data: bytes.toString("base64"),
21027
22274
  folder: options.folder,
21028
22275
  description: options.description
@@ -21223,7 +22470,7 @@ function registerMediaCommands(program2) {
21223
22470
  ).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
22471
  async (options, command) => {
21225
22472
  const flags = getGlobalFlags(command);
21226
- requireConfirm7(options.confirm, "generate an AI image (spends AI credits)");
22473
+ requireConfirm10(options.confirm, "generate an AI image (spends AI credits)");
21227
22474
  const input = {
21228
22475
  prompt: resolveTextOrFile(options.prompt),
21229
22476
  aspectRatio: parseEnumOption(
@@ -21255,7 +22502,7 @@ function registerMediaCommands(program2) {
21255
22502
  ).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
22503
  async (mediaId, options, command) => {
21257
22504
  const flags = getGlobalFlags(command);
21258
- requireConfirm7(options.confirm, `transcribe ${mediaId} (spends transcription minutes)`);
22505
+ requireConfirm10(options.confirm, `transcribe ${mediaId} (spends transcription minutes)`);
21259
22506
  const input = {};
21260
22507
  if (options.language !== void 0) input.language = options.language;
21261
22508
  if (options.force) input.force = true;
@@ -21329,13 +22576,13 @@ function registerMediaCommands(program2) {
21329
22576
  }
21330
22577
 
21331
22578
  // src/commands/newsletters/index.ts
21332
- import { existsSync as existsSync2, readFileSync as readFileSync16 } from "fs";
22579
+ import { existsSync as existsSync2, readFileSync as readFileSync18 } from "fs";
21333
22580
  var ISSUE_STATUSES = ["public", "hidden"];
21334
22581
  var ISSUE_VISIBILITIES = ["public", "hidden", "locked"];
21335
22582
  var ISSUE_RENDER_MODES = ["snapshot", "article"];
21336
22583
  var THUMBNAIL_NONE = "none";
21337
22584
  function resolveMarkdownOrFile(value) {
21338
- return existsSync2(value) ? readFileSync16(value, "utf8") : value;
22585
+ return existsSync2(value) ? readFileSync18(value, "utf8") : value;
21339
22586
  }
21340
22587
  function parseIdList2(value, flag) {
21341
22588
  const ids = value.split(",").map((id) => id.trim()).filter(Boolean);
@@ -21376,7 +22623,7 @@ function parseBool2(value, flag) {
21376
22623
  }
21377
22624
  function parseJsonFileOrInline5(value, flag) {
21378
22625
  const trimmed = value.trim();
21379
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync16(value, "utf8");
22626
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync18(value, "utf8");
21380
22627
  try {
21381
22628
  return JSON.parse(raw);
21382
22629
  } catch (error) {
@@ -21691,7 +22938,7 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
21691
22938
  }
21692
22939
 
21693
22940
  // src/commands/offers/index.ts
21694
- import { readFileSync as readFileSync17 } from "fs";
22941
+ import { readFileSync as readFileSync19 } from "fs";
21695
22942
  var PRICE_BILLING_TYPES = ["one_time", "recurring", "installments"];
21696
22943
  var PRICE_INTERVALS = ["day", "week", "month", "year"];
21697
22944
  var SALE_START_DISPLAYS = ["hidden", "countdown"];
@@ -21701,8 +22948,11 @@ var PRICE_OPERATIONS = [
21701
22948
  "deactivate",
21702
22949
  "hide",
21703
22950
  "unhide",
21704
- "change-amount"
22951
+ "change-amount",
22952
+ "edit-details"
21705
22953
  ];
22954
+ var EDIT_DETAILS_OPERATION = "edit-details";
22955
+ var CHANGE_AMOUNT_OPERATION = "change-amount";
21706
22956
  function parseChoice4(value, allowed, flag) {
21707
22957
  const match = allowed.find((candidate) => candidate === value);
21708
22958
  if (match === void 0) {
@@ -21710,27 +22960,154 @@ function parseChoice4(value, allowed, flag) {
21710
22960
  }
21711
22961
  return match;
21712
22962
  }
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)`);
22963
+ function parsePositiveInt12(value, flag) {
22964
+ const n = Number.parseInt(value, 10);
22965
+ if (!Number.isInteger(n) || n <= 0) {
22966
+ throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
22967
+ }
22968
+ return n;
22969
+ }
22970
+ function parseBool3(value, flag) {
22971
+ const v = value.trim().toLowerCase();
22972
+ if (["on", "true", "yes", "1"].includes(v)) return true;
22973
+ if (["off", "false", "no", "0"].includes(v)) return false;
22974
+ throw new Error(`Invalid ${flag}: ${value} (expected on/off, true/false, yes/no).`);
22975
+ }
22976
+ function parseJsonFileOrInline6(value, flag) {
22977
+ const trimmed = value.trim();
22978
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync19(value, "utf8");
22979
+ try {
22980
+ return JSON.parse(raw);
22981
+ } catch (error) {
22982
+ throw new Error(`Invalid JSON for ${flag}: ${error.message}`);
22983
+ }
22984
+ }
22985
+ function buildPriceUpdateInput(options) {
22986
+ const hasDetails = options.label !== void 0 || options.description !== void 0;
22987
+ const operation = options.operation !== void 0 ? parseChoice4(options.operation, PRICE_OPERATIONS, "--operation") : hasDetails ? EDIT_DETAILS_OPERATION : void 0;
22988
+ if (operation === void 0) {
22989
+ throw new Error(
22990
+ `--operation <${PRICE_OPERATIONS.join("|")}> is required (or pass --label / --description to edit the details).`
22991
+ );
22992
+ }
22993
+ if (hasDetails && operation !== EDIT_DETAILS_OPERATION) {
22994
+ throw new Error(
22995
+ `--label / --description only apply to --operation ${EDIT_DETAILS_OPERATION}, not ${operation}.`
22996
+ );
22997
+ }
22998
+ if (operation === EDIT_DETAILS_OPERATION) {
22999
+ if (!hasDetails) {
23000
+ throw new Error(
23001
+ `--operation ${EDIT_DETAILS_OPERATION} requires --label and/or --description.`
23002
+ );
23003
+ }
23004
+ if (options.label !== void 0 && options.label.trim().length === 0) {
23005
+ throw new Error("--label must not be empty (labels cannot be cleared, only replaced).");
23006
+ }
23007
+ return {
23008
+ operation,
23009
+ newLabel: options.label,
23010
+ // An empty string clears the description (the API accepts '' or null; null is explicit).
23011
+ newDescription: options.description === void 0 ? void 0 : options.description.length === 0 ? null : options.description
23012
+ };
23013
+ }
23014
+ if (operation === CHANGE_AMOUNT_OPERATION && !options.amount) {
23015
+ throw new Error(`--operation ${CHANGE_AMOUNT_OPERATION} requires --amount <minor-units>.`);
23016
+ }
23017
+ return {
23018
+ operation,
23019
+ newAmountCents: operation === CHANGE_AMOUNT_OPERATION && options.amount ? parsePositiveInt12(options.amount, "--amount") : void 0
23020
+ };
23021
+ }
23022
+ var MEDIA_TYPES = ["image", "video"];
23023
+ var IMAGE_MEDIA_TYPE2 = "image";
23024
+ var VIDEO_MEDIA_TYPE = "video";
23025
+ var READY_UPLOAD_STATUS = "ready";
23026
+ var GALLERY_KEYS = /* @__PURE__ */ new Set([
23027
+ "checkoutMedia",
23028
+ "checkoutImages",
23029
+ "checkoutImagesMedia"
23030
+ ]);
23031
+ var BASE_DETAIL_KEYS = /* @__PURE__ */ new Set([
23032
+ "id",
23033
+ "productId",
23034
+ "name",
23035
+ "slug",
23036
+ "title",
23037
+ "active",
23038
+ "archived",
23039
+ "isFree",
23040
+ "priceCount",
23041
+ "createdAt",
23042
+ "description",
23043
+ "language",
23044
+ "successUrl",
23045
+ "prices",
23046
+ "orderBumps"
23047
+ ]);
23048
+ function galleryOf(checkout) {
23049
+ if (checkout.checkoutMedia) return [...checkout.checkoutMedia];
23050
+ return (checkout.checkoutImages ?? []).map((url, index) => ({
23051
+ type: IMAGE_MEDIA_TYPE2,
23052
+ url,
23053
+ mediaId: checkout.checkoutImagesMedia?.[index]?.mediaId ?? null,
23054
+ thumbnailUrl: null
23055
+ }));
23056
+ }
23057
+ function galleryPatch(media) {
23058
+ const images = media.filter((item) => item.type === IMAGE_MEDIA_TYPE2);
23059
+ return {
23060
+ checkoutMedia: media,
23061
+ checkoutImages: images.map((item) => item.url),
23062
+ checkoutImagesMedia: images.map((item) => ({ url: item.url, mediaId: item.mediaId }))
23063
+ };
23064
+ }
23065
+ function bunnyEmbedUrl(video) {
23066
+ return `https://iframe.mediadelivery.net/embed/${video.bunnyLibraryId}/${video.bunnyVideoId}`;
23067
+ }
23068
+ function galleryEntryFromLibrary(item) {
23069
+ const type = MEDIA_TYPES.find((candidate) => candidate === item.type);
23070
+ if (type === void 0) {
23071
+ throw new Error(
23072
+ `Media ${item.id} is a ${item.type} \u2014 only images and videos can go in the offer gallery.`
23073
+ );
21717
23074
  }
21718
- return n;
23075
+ if (type === VIDEO_MEDIA_TYPE) {
23076
+ if (item.uploadStatus !== READY_UPLOAD_STATUS || !item.video) {
23077
+ throw new Error(
23078
+ `Media ${item.id} is a video that is not ready yet (uploadStatus: ${item.uploadStatus}) \u2014 add it once it finishes processing.`
23079
+ );
23080
+ }
23081
+ return {
23082
+ type,
23083
+ url: bunnyEmbedUrl(item.video),
23084
+ mediaId: item.id,
23085
+ thumbnailUrl: item.thumbnailUrl ?? null
23086
+ };
23087
+ }
23088
+ if (!item.url) {
23089
+ throw new Error(`Media ${item.id} has no public URL yet (uploadStatus: ${item.uploadStatus}).`);
23090
+ }
23091
+ return { type, url: item.url, mediaId: item.id, thumbnailUrl: item.thumbnailUrl ?? null };
21719
23092
  }
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).`);
23093
+ function printGallery(media) {
23094
+ printTable(
23095
+ media.map((item, index) => ({
23096
+ index,
23097
+ type: item.type,
23098
+ mediaId: item.mediaId ?? "",
23099
+ url: item.url,
23100
+ thumbnailUrl: item.thumbnailUrl ?? ""
23101
+ })),
23102
+ ["index", "type", "mediaId", "url", "thumbnailUrl"]
23103
+ );
21725
23104
  }
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
- }
23105
+ function fullSettingsOf(checkout) {
23106
+ const isRendered = (key) => {
23107
+ const known = key;
23108
+ return BASE_DETAIL_KEYS.has(known) || GALLERY_KEYS.has(known);
23109
+ };
23110
+ return Object.fromEntries(Object.entries(checkout).filter(([key]) => !isRendered(key)));
21734
23111
  }
21735
23112
  function formatAmount2(amountCents, currency) {
21736
23113
  return `${(amountCents / 100).toFixed(2)} ${currency}`;
@@ -21984,10 +23361,12 @@ function registerOffersCommands(program2) {
21984
23361
  );
21985
23362
  });
21986
23363
  });
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) => {
23364
+ offers.command("get").description(
23365
+ "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"
23366
+ ).argument("<checkoutId>", "offer (checkout) id").option("--full", "include the whole editable settings surface (?include=full)").action(async (checkoutId, options, command) => {
21988
23367
  const flags = getGlobalFlags(command);
21989
23368
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21990
- const checkout = await client.getCheckout(checkoutId);
23369
+ const checkout = await client.getCheckout(checkoutId, { full: options.full === true });
21991
23370
  emit(flags, checkout, () => {
21992
23371
  process.stdout.write(`Offer ${checkout.id} \u2014 ${checkout.name}
21993
23372
  `);
@@ -22047,6 +23426,14 @@ Order bumps (${checkout.orderBumps.length}):
22047
23426
  process.stdout.write(` ${bump.orderBumpId} (${scope})
22048
23427
  `);
22049
23428
  }
23429
+ if (!options.full) return;
23430
+ const gallery = galleryOf(checkout);
23431
+ process.stdout.write(`
23432
+ Media gallery (${gallery.length}):
23433
+ `);
23434
+ printGallery(gallery);
23435
+ process.stdout.write("\nSettings (--full):\n");
23436
+ printJson(fullSettingsOf(checkout));
22050
23437
  });
22051
23438
  });
22052
23439
  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 +23642,23 @@ Order bumps (${checkout.orderBumps.length}):
22255
23642
  );
22256
23643
  });
22257
23644
  });
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(
23645
+ price.command("update").description(
23646
+ "Mutate a price variant: --operation activate | deactivate | hide | unhide | change-amount (with --amount) | edit-details (with --label / --description). --label / --description alone imply edit-details"
23647
+ ).argument("<checkoutId>", "offer (checkout) id").argument("<priceId>", "price id").option(
23648
+ "--operation <op>",
23649
+ "activate | deactivate | hide | unhide | change-amount | edit-details (implied by --label / --description)"
23650
+ ).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(
23651
+ "--description <markdown>",
23652
+ "edit-details: new markdown description under the price (empty string clears it)"
23653
+ ).action(
22259
23654
  async (checkoutId, priceId, options, command) => {
22260
23655
  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
- }
23656
+ const input = buildPriceUpdateInput(options);
22265
23657
  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
- });
23658
+ const result = await client.updateCheckoutPrice(checkoutId, priceId, input);
22270
23659
  emit(flags, result, () => {
22271
23660
  process.stdout.write(
22272
- `Applied ${operation} to price ${result.priceId} on offer ${result.checkoutId}
23661
+ `Applied ${input.operation} to price ${result.priceId} on offer ${result.checkoutId}
22273
23662
  `
22274
23663
  );
22275
23664
  });
@@ -22305,6 +23694,87 @@ Order bumps (${checkout.orderBumps.length}):
22305
23694
  `);
22306
23695
  });
22307
23696
  });
23697
+ const media = offers.command("media").description("Manage an offer's media gallery (checkout images and videos)");
23698
+ 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) => {
23699
+ const flags = getGlobalFlags(command);
23700
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23701
+ const checkout = await client.getCheckout(checkoutId, { full: true });
23702
+ const gallery = galleryOf(checkout);
23703
+ emit(flags, { checkoutId: checkout.id, count: gallery.length, media: gallery }, () => {
23704
+ process.stdout.write(`Media gallery of offer ${checkout.id} (${gallery.length}):
23705
+ `);
23706
+ printGallery(gallery);
23707
+ });
23708
+ });
23709
+ media.command("add").description(
23710
+ "Append a Media Library item (--media <mediaId>; videos must be ready) or an external URL (--url with --type) to the gallery"
23711
+ ).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(
23712
+ async (checkoutId, options, command) => {
23713
+ if (options.media === void 0 === (options.url === void 0)) {
23714
+ throw new Error("Pass exactly one of --media <mediaId> or --url <url>.");
23715
+ }
23716
+ if (options.media !== void 0 && (options.type !== void 0 || options.thumbnail)) {
23717
+ throw new Error(
23718
+ "--type / --thumbnail only apply with --url (a library item carries its own)."
23719
+ );
23720
+ }
23721
+ const flags = getGlobalFlags(command);
23722
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23723
+ let entry;
23724
+ if (options.media !== void 0) {
23725
+ const { media: item } = await client.getMedia(options.media);
23726
+ entry = galleryEntryFromLibrary(item);
23727
+ } else {
23728
+ if (options.type === void 0) {
23729
+ throw new Error("--url requires --type <image|video>.");
23730
+ }
23731
+ entry = {
23732
+ type: parseChoice4(options.type, MEDIA_TYPES, "--type"),
23733
+ url: options.url ?? "",
23734
+ mediaId: null,
23735
+ thumbnailUrl: options.thumbnail ?? null
23736
+ };
23737
+ }
23738
+ const checkout = await client.getCheckout(checkoutId, { full: true });
23739
+ const gallery = galleryOf(checkout);
23740
+ const duplicate = gallery.find(
23741
+ (item) => entry.mediaId !== null && item.mediaId === entry.mediaId || item.url === entry.url
23742
+ );
23743
+ if (duplicate) {
23744
+ throw new Error(
23745
+ `Already in the gallery: ${duplicate.mediaId ?? duplicate.url} (index ${gallery.indexOf(duplicate)}).`
23746
+ );
23747
+ }
23748
+ const next = [...gallery, entry];
23749
+ const result = await client.updateCheckout(checkoutId, galleryPatch(next));
23750
+ emit(flags, { checkoutId: result.checkoutId, added: entry, media: next }, () => {
23751
+ process.stdout.write(
23752
+ `Added ${entry.type} ${entry.mediaId ?? entry.url} to offer ${result.checkoutId} (gallery: ${next.length})
23753
+ `
23754
+ );
23755
+ });
23756
+ }
23757
+ );
23758
+ 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) => {
23759
+ const flags = getGlobalFlags(command);
23760
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23761
+ const checkout = await client.getCheckout(checkoutId, { full: true });
23762
+ const gallery = galleryOf(checkout);
23763
+ const removed = gallery.filter((item) => item.mediaId === ref || item.url === ref);
23764
+ if (removed.length === 0) {
23765
+ throw new Error(
23766
+ `No gallery entry matches ${ref} on offer ${checkoutId} \u2014 run \`offers media list ${checkoutId}\`.`
23767
+ );
23768
+ }
23769
+ const next = gallery.filter((item) => !removed.includes(item));
23770
+ const result = await client.updateCheckout(checkoutId, galleryPatch(next));
23771
+ emit(flags, { checkoutId: result.checkoutId, removed, media: next }, () => {
23772
+ process.stdout.write(
23773
+ `Removed ${removed.length} ${removed.length === 1 ? "entry" : "entries"} (${ref}) from offer ${result.checkoutId} (gallery: ${next.length})
23774
+ `
23775
+ );
23776
+ });
23777
+ });
22308
23778
  }
22309
23779
 
22310
23780
  // src/commands/orders/index.ts
@@ -22326,7 +23796,7 @@ function parsePositiveInt13(value, flag) {
22326
23796
  function formatAmount3(amountCents, currency) {
22327
23797
  return `${(amountCents / 100).toFixed(2)} ${currency}`;
22328
23798
  }
22329
- function requireConfirm8(confirm, what) {
23799
+ function requireConfirm11(confirm, what) {
22330
23800
  if (!confirm) {
22331
23801
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
22332
23802
  }
@@ -22425,7 +23895,7 @@ Next page: --cursor ${page.nextCursor}
22425
23895
  ).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
23896
  async (orderId, options, command) => {
22427
23897
  const flags = getGlobalFlags(command);
22428
- requireConfirm8(options.confirm, `approve order ${orderId} (access + purchase email)`);
23898
+ requireConfirm11(options.confirm, `approve order ${orderId} (access + purchase email)`);
22429
23899
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22430
23900
  const result = await client.approveManualOrder(orderId, {
22431
23901
  issueInvoice: options.issueInvoice === true ? true : void 0
@@ -22442,7 +23912,7 @@ Next page: --cursor ${page.nextCursor}
22442
23912
  "Reject a manual (wire-transfer) order \u2014 cancels it and notifies the buyer. 409 = the order was already decided. Requires --confirm"
22443
23913
  ).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
23914
  const flags = getGlobalFlags(command);
22445
- requireConfirm8(options.confirm, `reject order ${orderId} (cancel + notify the buyer)`);
23915
+ requireConfirm11(options.confirm, `reject order ${orderId} (cancel + notify the buyer)`);
22446
23916
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22447
23917
  const result = await client.rejectManualOrder(orderId);
22448
23918
  emit(flags, result, () => {
@@ -22492,12 +23962,12 @@ Next page: --cursor ${page.nextCursor}
22492
23962
  }
22493
23963
 
22494
23964
  // src/commands/oto/index.ts
22495
- import { readFileSync as readFileSync18 } from "fs";
23965
+ import { readFileSync as readFileSync20 } from "fs";
22496
23966
  var MIN_TIMER_MINUTES = 1;
22497
23967
  var MAX_TIMER_MINUTES = 120;
22498
23968
  function parseJsonFileOrInline7(value, flag) {
22499
23969
  const trimmed = value.trim();
22500
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync18(value, "utf8");
23970
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync20(value, "utf8");
22501
23971
  try {
22502
23972
  return JSON.parse(raw);
22503
23973
  } catch (error) {
@@ -22685,7 +24155,7 @@ function registerOtoCommands(program2) {
22685
24155
  }
22686
24156
 
22687
24157
  // src/commands/pages/index.ts
22688
- import { existsSync as existsSync3, readFileSync as readFileSync19 } from "fs";
24158
+ import { existsSync as existsSync3, readFileSync as readFileSync21 } from "fs";
22689
24159
  import { readFile as readFile2 } from "fs/promises";
22690
24160
  async function resolveHtml2(options) {
22691
24161
  if (options.file) {
@@ -22732,13 +24202,13 @@ function parseCsv4(value) {
22732
24202
  ];
22733
24203
  }
22734
24204
  function resolveTextOrFile2(value) {
22735
- const text = existsSync3(value) ? readFileSync19(value, "utf8") : value;
24205
+ const text = existsSync3(value) ? readFileSync21(value, "utf8") : value;
22736
24206
  if (!text.trim()) throw new Error("The text (or the file it points to) is empty.");
22737
24207
  return text;
22738
24208
  }
22739
24209
  function parseJsonObjectFileOrInline3(value, flag) {
22740
24210
  const trimmed = value.trim();
22741
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync19(value, "utf8");
24211
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync21(value, "utf8");
22742
24212
  let parsed;
22743
24213
  try {
22744
24214
  parsed = JSON.parse(raw);
@@ -22750,7 +24220,7 @@ function parseJsonObjectFileOrInline3(value, flag) {
22750
24220
  }
22751
24221
  return parsed;
22752
24222
  }
22753
- function requireConfirm9(confirm, what) {
24223
+ function requireConfirm12(confirm, what) {
22754
24224
  if (!confirm) {
22755
24225
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
22756
24226
  }
@@ -22945,7 +24415,7 @@ function registerPagesCommands(program2) {
22945
24415
  "inline JSON object or .json path merged into the request body (explicit flags win)"
22946
24416
  ).option("--confirm", "required \u2014 acknowledge that AI credits will be consumed").action(async (options, command) => {
22947
24417
  const flags = getGlobalFlags(command);
22948
- requireConfirm9(options.confirm, "generate a page (consumes AI credits)");
24418
+ requireConfirm12(options.confirm, "generate a page (consumes AI credits)");
22949
24419
  const input = buildGenerateInput(options);
22950
24420
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22951
24421
  const result = await client.generatePage(input).catch(withSchedulerHint3);
@@ -22967,7 +24437,7 @@ function registerPagesCommands(program2) {
22967
24437
  ).action(
22968
24438
  async (pageId, options, command) => {
22969
24439
  const flags = getGlobalFlags(command);
22970
- requireConfirm9(
24440
+ requireConfirm12(
22971
24441
  options.confirm,
22972
24442
  "AI-edit the page (consumes AI credits, replaces the HTML)"
22973
24443
  );
@@ -23026,7 +24496,7 @@ function registerPagesCommands(program2) {
23026
24496
  ).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
24497
  async (pageId, revisionId, options, command) => {
23028
24498
  const flags = getGlobalFlags(command);
23029
- requireConfirm9(options.confirm, "restore the revision (replaces the current HTML)");
24499
+ requireConfirm12(options.confirm, "restore the revision (replaces the current HTML)");
23030
24500
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23031
24501
  const result = await client.restorePageRevision(pageId, revisionId);
23032
24502
  emit(flags, result, () => {
@@ -23039,7 +24509,7 @@ function registerPagesCommands(program2) {
23039
24509
  "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
24510
  ).argument("<pageId>", "page id").option("--confirm", "required \u2014 acknowledge that automations go live and emails will send").action(async (pageId, options, command) => {
23041
24511
  const flags = getGlobalFlags(command);
23042
- requireConfirm9(options.confirm, "activate the page automations (emails will send)");
24512
+ requireConfirm12(options.confirm, "activate the page automations (emails will send)");
23043
24513
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23044
24514
  const result = await client.activatePageAutomations(pageId);
23045
24515
  emit(flags, result, () => {
@@ -23069,7 +24539,9 @@ function registerPagesCommands(program2) {
23069
24539
  }
23070
24540
 
23071
24541
  // src/commands/products/index.ts
23072
- import { readFileSync as readFileSync20 } from "fs";
24542
+ import { readFileSync as readFileSync22 } from "fs";
24543
+ import { readFile as readFile3 } from "fs/promises";
24544
+ import { basename as basename2 } from "path";
23073
24545
  var KB_TREE_CHANGED_ERROR = "tree-changed";
23074
24546
  var JSON_FLAG_HINT3 = "(write it as --json=<value> \u2014 a bare --json is the global JSON-output flag)";
23075
24547
  function parseCoverRef(value) {
@@ -23175,7 +24647,7 @@ function assertOneOf3(value, allowed, flag) {
23175
24647
  }
23176
24648
  function parseJsonFileOrInline8(value, flag) {
23177
24649
  const trimmed = value.trim();
23178
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync20(value, "utf8");
24650
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync22(value, "utf8");
23179
24651
  try {
23180
24652
  return JSON.parse(raw);
23181
24653
  } catch (error) {
@@ -23185,6 +24657,49 @@ function parseJsonFileOrInline8(value, flag) {
23185
24657
  function collectRepeatable5(value, previous) {
23186
24658
  return [...previous, value];
23187
24659
  }
24660
+ var HTTP_URL_PATTERN2 = /^https?:\/\//i;
24661
+ async function uploadLocalFile(client, filePath) {
24662
+ const bytes = await readFile3(filePath);
24663
+ const name = basename2(filePath);
24664
+ const result = await client.uploadMedia({
24665
+ name,
24666
+ mimeType: inferMimeType(name, bytes),
24667
+ data: bytes.toString("base64")
24668
+ });
24669
+ return { ...result, name, size: bytes.length };
24670
+ }
24671
+ async function resolveCoverUrl(client, value) {
24672
+ if (HTTP_URL_PATTERN2.test(value)) return value;
24673
+ const uploaded = await uploadLocalFile(client, value);
24674
+ return uploaded.url;
24675
+ }
24676
+ async function setProductCoverImages(client, productId, urls) {
24677
+ for (const url of urls) assertHttpsUrl(url, "--image");
24678
+ return client.setProductImages(productId, urls);
24679
+ }
24680
+ function printFilesTable(rows) {
24681
+ printTable(
24682
+ rows.map((f) => ({
24683
+ uid: f.uid,
24684
+ name: f.name,
24685
+ url: f.url,
24686
+ mediaId: f.mediaId ?? "",
24687
+ size: f.size ?? ""
24688
+ })),
24689
+ ["uid", "name", "url", "mediaId", "size"]
24690
+ );
24691
+ }
24692
+ async function addProductFiles(client, productId, inputs) {
24693
+ return client.addDownloadFiles(productId, inputs);
24694
+ }
24695
+ function printAddedFiles(inputCount, result) {
24696
+ const skipped = inputCount - result.added.length;
24697
+ process.stdout.write(
24698
+ `Added ${result.added.length} file(s)${skipped > 0 ? ` (${skipped} skipped \u2014 already attached)` : ""}; ${result.files.length} total.
24699
+ `
24700
+ );
24701
+ printFilesTable(result.files);
24702
+ }
23188
24703
  function registerProductsCommands(program2) {
23189
24704
  const products2 = program2.command("products").description("Manage products");
23190
24705
  products2.command("list").description("List products in the workspace").action(async (_, command) => {
@@ -23230,18 +24745,54 @@ function registerProductsCommands(program2) {
23230
24745
  ).requiredOption("--name <name>", "product name").requiredOption("--type <type>", "product type (course, newsletter, community, ...)").option("--description <description>", "product description").option("--free", "seed a free default offer").option("--amount <amount>", "seed a paid default offer \u2014 price in MAJOR units (49.99)").option("--currency <currency>", "currency for --amount (pln, usd, eur, gbp)").option(
23231
24746
  "--period <period>",
23232
24747
  "billing period for --amount: one_time, monthly or yearly (default one_time)"
24748
+ ).option("--description-file <path>", "read the product description from a file").option(
24749
+ "--cover <pathOrUrl>",
24750
+ "cover image: a local file (uploaded to the media library first) or an https URL"
24751
+ ).option(
24752
+ "--file <path>",
24753
+ "local file to upload and attach to the product (downloads products; repeat for several)",
24754
+ collectRepeatable5,
24755
+ []
23233
24756
  ).action(
23234
24757
  async (options, command) => {
23235
24758
  const flags = getGlobalFlags(command);
23236
24759
  const offer = parseCreateOfferFlags(options);
24760
+ if (options.description !== void 0 && options.descriptionFile !== void 0) {
24761
+ throw new Error("Use either --description or --description-file, not both.");
24762
+ }
24763
+ const description = options.descriptionFile !== void 0 ? readFileSync22(options.descriptionFile, "utf8") : options.description;
23237
24764
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23238
24765
  const product = await client.createProduct({
23239
24766
  name: options.name,
23240
24767
  productType: options.type,
23241
- description: options.description,
24768
+ description,
23242
24769
  offer
23243
24770
  });
23244
- emit(flags, product, () => {
24771
+ const outcome = {};
24772
+ try {
24773
+ if (options.cover !== void 0) {
24774
+ const coverUrl = await resolveCoverUrl(client, options.cover);
24775
+ outcome.cover = await setProductCoverImages(client, product.id, [coverUrl]);
24776
+ }
24777
+ if (options.file.length > 0) {
24778
+ const uploaded = [];
24779
+ for (const filePath of options.file)
24780
+ uploaded.push(await uploadLocalFile(client, filePath));
24781
+ outcome.uploadedFiles = uploaded;
24782
+ outcome.files = await addProductFiles(
24783
+ client,
24784
+ product.id,
24785
+ uploaded.map((u) => ({ url: u.url, name: u.name, mediaId: u.mediaId, size: u.size }))
24786
+ );
24787
+ }
24788
+ } catch (error) {
24789
+ process.stderr.write(
24790
+ `Product ${product.id} was created, but a follow-up step failed \u2014 fix it with \`products images set\` / \`products files add\`.
24791
+ `
24792
+ );
24793
+ throw error;
24794
+ }
24795
+ emit(flags, { ...product, ...outcome }, () => {
23245
24796
  process.stdout.write(`Created product ${product.id} \u2014 ${product.name}
23246
24797
  `);
23247
24798
  if (offer) {
@@ -23250,6 +24801,17 @@ function registerProductsCommands(program2) {
23250
24801
  ` : " Default offer was requested but not seeded (check the dashboard).\n"
23251
24802
  );
23252
24803
  }
24804
+ if (outcome.cover) {
24805
+ process.stdout.write(` Cover image: ${outcome.cover.images.join(", ")}
24806
+ `);
24807
+ }
24808
+ if (outcome.uploadedFiles && outcome.files) {
24809
+ process.stdout.write(
24810
+ ` Uploaded ${outcome.uploadedFiles.length} file(s): ${outcome.uploadedFiles.map((u) => `${u.name} (${u.mediaId})`).join(", ")}
24811
+ `
24812
+ );
24813
+ printAddedFiles(outcome.uploadedFiles.length, outcome.files);
24814
+ }
23253
24815
  });
23254
24816
  }
23255
24817
  );
@@ -23727,9 +25289,8 @@ function registerProductsCommands(program2) {
23727
25289
  if (options.image.length === 0) {
23728
25290
  throw new Error("Pass at least one --image <url>.");
23729
25291
  }
23730
- for (const url of options.image) assertHttpsUrl(url, "--image");
23731
25292
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23732
- const result = await client.setProductImages(productId, options.image);
25293
+ const result = await setProductCoverImages(client, productId, options.image);
23733
25294
  emit(flags, result, () => {
23734
25295
  process.stdout.write(
23735
25296
  `Set ${result.images.length} cover image(s) on product ${productId}.
@@ -23738,18 +25299,6 @@ function registerProductsCommands(program2) {
23738
25299
  });
23739
25300
  });
23740
25301
  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
25302
  files.command("get").description("Show the downloads content: description presence + file table").requiredOption("--product <id>", "downloads product id").action(async (options, command) => {
23754
25303
  const flags = getGlobalFlags(command);
23755
25304
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -23770,7 +25319,7 @@ function registerProductsCommands(program2) {
23770
25319
  if (options.content !== void 0 && options.contentFile !== void 0) {
23771
25320
  throw new Error("Use either --content or --content-file, not both.");
23772
25321
  }
23773
- const editorContent = options.contentFile !== void 0 ? readFileSync20(options.contentFile, "utf8") : options.content;
25322
+ const editorContent = options.contentFile !== void 0 ? readFileSync22(options.contentFile, "utf8") : options.content;
23774
25323
  if (editorContent === void 0) {
23775
25324
  throw new Error("Pass the description via --content or --content-file.");
23776
25325
  }
@@ -23816,14 +25365,9 @@ function registerProductsCommands(program2) {
23816
25365
  ];
23817
25366
  }
23818
25367
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23819
- const result = await client.addDownloadFiles(options.product, inputs);
25368
+ const result = await addProductFiles(client, options.product, inputs);
23820
25369
  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);
25370
+ printAddedFiles(inputs.length, result);
23827
25371
  });
23828
25372
  }
23829
25373
  );
@@ -23869,7 +25413,7 @@ function registerProductsCommands(program2) {
23869
25413
  if (options.content !== void 0 && options.contentFile !== void 0) {
23870
25414
  throw new Error("Use either --content or --content-file, not both.");
23871
25415
  }
23872
- return options.contentFile !== void 0 ? readFileSync20(options.contentFile, "utf8") : options.content;
25416
+ return options.contentFile !== void 0 ? readFileSync22(options.contentFile, "utf8") : options.content;
23873
25417
  };
23874
25418
  kb.command("get").description(
23875
25419
  "Show the knowledge base: categories (tree keys = the --category ids) + all articles (drafts and unavailable included \u2014 the creator view)"
@@ -24089,7 +25633,7 @@ function registerProductsCommands(program2) {
24089
25633
  }
24090
25634
 
24091
25635
  // src/commands/referrals/index.ts
24092
- import { readFileSync as readFileSync21 } from "fs";
25636
+ import { readFileSync as readFileSync23 } from "fs";
24093
25637
  var PROGRAM_KINDS = ["referral", "affiliate"];
24094
25638
  var WRITABLE_STATUSES = ["active", "inactive"];
24095
25639
  var ENROLLMENT_MODES = ["open", "inviteOnly", "closed"];
@@ -24114,7 +25658,7 @@ function parseIdList3(value, flag) {
24114
25658
  }
24115
25659
  function parseJsonObjectFileOrInline4(value, flag) {
24116
25660
  const trimmed = value.trim();
24117
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync21(value, "utf8");
25661
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync23(value, "utf8");
24118
25662
  let parsed;
24119
25663
  try {
24120
25664
  parsed = JSON.parse(raw);
@@ -24152,7 +25696,7 @@ function parseDeepConfig(value) {
24152
25696
  }
24153
25697
  return parsed;
24154
25698
  }
24155
- function requireConfirm10(confirm, what) {
25699
+ function requireConfirm13(confirm, what) {
24156
25700
  if (!confirm) {
24157
25701
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
24158
25702
  }
@@ -24387,7 +25931,7 @@ Failed deliveries (${result.failedDeliveries.length})
24387
25931
  "Archive a program \u2014 new referrals stop counting and it leaves the active list. Requires --confirm."
24388
25932
  ).argument("<programId>", "program id").option("--confirm", "required \u2014 acknowledge the archive").action(async (programId, options, command) => {
24389
25933
  const flags = getGlobalFlags(command);
24390
- requireConfirm10(options.confirm, `archive program ${programId}`);
25934
+ requireConfirm13(options.confirm, `archive program ${programId}`);
24391
25935
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24392
25936
  const result = await client.archiveReferralProgram(programId);
24393
25937
  emit(flags, result, () => {
@@ -24419,7 +25963,7 @@ Failed deliveries (${result.failedDeliveries.length})
24419
25963
  "Enroll EVERY client with access to one of the program products. Requires --confirm. Reports enrolled / alreadyEnrolled / skippedWithoutAccount counts."
24420
25964
  ).argument("<programId>", "program id").option("--confirm", "required \u2014 acknowledge the bulk enrollment").action(async (programId, options, command) => {
24421
25965
  const flags = getGlobalFlags(command);
24422
- requireConfirm10(options.confirm, `enroll all clients in program ${programId}`);
25966
+ requireConfirm13(options.confirm, `enroll all clients in program ${programId}`);
24423
25967
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24424
25968
  const result = await client.enrollAllReferralClients(programId);
24425
25969
  emit(flags, result, () => {
@@ -24435,7 +25979,7 @@ Failed deliveries (${result.failedDeliveries.length})
24435
25979
  async (programId, entryId, options, command) => {
24436
25980
  const flags = getGlobalFlags(command);
24437
25981
  const decision = parseChoice6(options.decision, REWARD_DECISIONS, "--decision");
24438
- requireConfirm10(options.confirm, `${decision} reward ${entryId} in program ${programId}`);
25982
+ requireConfirm13(options.confirm, `${decision} reward ${entryId} in program ${programId}`);
24439
25983
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24440
25984
  const result = await client.resolveReferralReward(programId, entryId, { decision });
24441
25985
  emit(flags, result, () => {
@@ -24465,7 +26009,7 @@ Failed deliveries (${result.failedDeliveries.length})
24465
26009
  }
24466
26010
 
24467
26011
  // src/commands/segments/index.ts
24468
- import { readFileSync as readFileSync22 } from "fs";
26012
+ import { readFileSync as readFileSync24 } from "fs";
24469
26013
  var SUBSCRIPTION_STATUSES2 = ["pending", "subscribed", "unsubscribed"];
24470
26014
  function collectRepeatable6(value, previous) {
24471
26015
  return [...previous, value];
@@ -24477,14 +26021,14 @@ function parseMinorUnits(value, flag) {
24477
26021
  }
24478
26022
  return n;
24479
26023
  }
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";
26024
+ 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
26025
  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;
26026
+ 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
26027
  if (options.jsonFile !== void 0) {
24484
26028
  if (hasFlagConditions) {
24485
26029
  throw new Error("Use either condition flags or --json-file, not both.");
24486
26030
  }
24487
- const raw = readFileSync22(options.jsonFile, "utf8");
26031
+ const raw = readFileSync24(options.jsonFile, "utf8");
24488
26032
  let parsed;
24489
26033
  try {
24490
26034
  parsed = JSON.parse(raw);
@@ -24506,6 +26050,12 @@ function resolveConditions(options) {
24506
26050
  for (const tagId of options.withoutTag) {
24507
26051
  conditions.push({ field: "tag", operator: "not-has", tagId });
24508
26052
  }
26053
+ if (options.hasAnyTag.length > 0) {
26054
+ conditions.push({ field: "tag", operator: "has-any", tagIds: options.hasAnyTag });
26055
+ }
26056
+ if (options.hasNoneTag.length > 0) {
26057
+ conditions.push({ field: "tag", operator: "has-none", tagIds: options.hasNoneTag });
26058
+ }
24509
26059
  for (const productId of options.owns) {
24510
26060
  conditions.push({ field: "product", operator: "owns", productId });
24511
26061
  }
@@ -24554,6 +26104,16 @@ function withConditionOptions(command) {
24554
26104
  "client does NOT carry this tag (repeatable)",
24555
26105
  collectRepeatable6,
24556
26106
  []
26107
+ ).option(
26108
+ "--has-any-tag <tagId>",
26109
+ "client carries AT LEAST ONE of these tags (repeat the flag; one OR condition, max 10 ids) - one send to several tags without double-mailing",
26110
+ collectRepeatable6,
26111
+ []
26112
+ ).option(
26113
+ "--has-none-tag <tagId>",
26114
+ "client carries NONE of these tags (repeat the flag; one condition, max 10 ids)",
26115
+ collectRepeatable6,
26116
+ []
24557
26117
  ).option(
24558
26118
  "--owns <productId>",
24559
26119
  "client has ACTIVE access to this product - a direct grant OR through a bundle (repeatable)",
@@ -24588,6 +26148,9 @@ function withConditionOptions(command) {
24588
26148
  function describeCondition(condition) {
24589
26149
  switch (condition.field) {
24590
26150
  case "tag":
26151
+ if ("tagIds" in condition) {
26152
+ return `${condition.operator === "has-any" ? "has any of" : "has none of"} tags ${condition.tagIds.join(", ")}`;
26153
+ }
24591
26154
  return `${condition.operator === "has" ? "has" : "lacks"} tag ${condition.tagId}`;
24592
26155
  case "product":
24593
26156
  return `${condition.operator === "owns" ? "owns" : "does not own"} product ${condition.productId}${condition.accessScope === "direct" ? " (direct)" : ""}`;
@@ -24700,7 +26263,7 @@ function registerSegmentsCommands(program2) {
24700
26263
  }
24701
26264
 
24702
26265
  // src/commands/site/index.ts
24703
- import { existsSync as existsSync4, readFileSync as readFileSync23 } from "fs";
26266
+ import { existsSync as existsSync4, readFileSync as readFileSync25 } from "fs";
24704
26267
  var MAIN_PAGE_MODES = {
24705
26268
  sales: "/",
24706
26269
  dashboard: "/dashboard",
@@ -24724,14 +26287,14 @@ var HTTP_CONFLICT = 409;
24724
26287
  var HTTP_BAD_REQUEST = 400;
24725
26288
  var HTTP_NOT_FOUND = 404;
24726
26289
  var HTTP_FORBIDDEN = 403;
24727
- function requireConfirm11(confirm, what) {
26290
+ function requireConfirm14(confirm, what) {
24728
26291
  if (!confirm) {
24729
26292
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
24730
26293
  }
24731
26294
  }
24732
26295
  function parseJsonObjectFileOrInline5(value, flag) {
24733
26296
  const trimmed = value.trim();
24734
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync23(value, "utf8");
26297
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync25(value, "utf8");
24735
26298
  let parsed;
24736
26299
  try {
24737
26300
  parsed = JSON.parse(raw);
@@ -24746,7 +26309,7 @@ function parseJsonObjectFileOrInline5(value, flag) {
24746
26309
  function baseBody(json) {
24747
26310
  return json === void 0 ? {} : parseJsonObjectFileOrInline5(json, "--json");
24748
26311
  }
24749
- function parseOnOff2(value, flag) {
26312
+ function parseOnOff4(value, flag) {
24750
26313
  const v = value.trim().toLowerCase();
24751
26314
  if (v === "on" || v === "true") return true;
24752
26315
  if (v === "off" || v === "false") return false;
@@ -24783,7 +26346,7 @@ function parseDomainList(value, what) {
24783
26346
  return items;
24784
26347
  }
24785
26348
  function readMarkdownOrFile(value) {
24786
- return existsSync4(value) ? readFileSync23(value, "utf8") : value;
26349
+ return existsSync4(value) ? readFileSync25(value, "utf8") : value;
24787
26350
  }
24788
26351
  function assertNonEmptyBody(body, hint) {
24789
26352
  if (Object.keys(body).length === 0) {
@@ -24990,7 +26553,7 @@ function registerSiteCommands(program2) {
24990
26553
  body.language = parseChoice7(options.language, PLATFORM_LANGUAGES, "--language");
24991
26554
  }
24992
26555
  if (options.showPageTitle !== void 0) {
24993
- body.showPageTitle = parseOnOff2(options.showPageTitle, "--show-page-title");
26556
+ body.showPageTitle = parseOnOff4(options.showPageTitle, "--show-page-title");
24994
26557
  }
24995
26558
  assertNonEmptyBody(body, "pass --title, --description, --language or --json=<patch>.");
24996
26559
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -25012,7 +26575,7 @@ function registerSiteCommands(program2) {
25012
26575
  if (options.radius !== void 0) body.radius = options.radius;
25013
26576
  if (options.styleVariant !== void 0) body.styleVariant = options.styleVariant;
25014
26577
  if (options.fromBrand !== void 0) {
25015
- body.themeFromBrand = parseOnOff2(options.fromBrand, "--from-brand");
26578
+ body.themeFromBrand = parseOnOff4(options.fromBrand, "--from-brand");
25016
26579
  }
25017
26580
  assertNonEmptyBody(body, "pass --theme, --radius, --from-brand \u2026 or --json=<patch>.");
25018
26581
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -25039,7 +26602,7 @@ function registerSiteCommands(program2) {
25039
26602
  body.platformFaviconUrl = orNull6(options.faviconUrl);
25040
26603
  }
25041
26604
  if (options.hideBranding !== void 0) {
25042
- body.hideZanfiaBranding = parseOnOff2(options.hideBranding, "--hide-branding");
26605
+ body.hideZanfiaBranding = parseOnOff4(options.hideBranding, "--hide-branding");
25043
26606
  }
25044
26607
  assertNonEmptyBody(
25045
26608
  body,
@@ -25084,7 +26647,7 @@ function registerSiteCommands(program2) {
25084
26647
  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
26648
  const flags = getGlobalFlags(command);
25086
26649
  const base = baseBody(options.json);
25087
- const cookieConsentEnabled = options.cookieConsent !== void 0 ? parseOnOff2(options.cookieConsent, "--cookie-consent") : base.cookieConsentEnabled;
26650
+ const cookieConsentEnabled = options.cookieConsent !== void 0 ? parseOnOff4(options.cookieConsent, "--cookie-consent") : base.cookieConsentEnabled;
25088
26651
  if (typeof cookieConsentEnabled !== "boolean") {
25089
26652
  throw new Error(
25090
26653
  'Pass --cookie-consent on|off (or a --json body with "cookieConsentEnabled").'
@@ -25151,14 +26714,14 @@ function registerSiteCommands(program2) {
25151
26714
  if (options.libraryOrder === void 0) customization.libraryOrder = "custom";
25152
26715
  }
25153
26716
  if (options.groupByType !== void 0) {
25154
- customization.libraryGroupByType = parseOnOff2(options.groupByType, "--group-by-type");
26717
+ customization.libraryGroupByType = parseOnOff4(options.groupByType, "--group-by-type");
25155
26718
  }
25156
26719
  if (options.defaultView !== void 0) customization.defaultView = options.defaultView;
25157
26720
  if (options.messages !== void 0) {
25158
- customization.messagesEnabled = parseOnOff2(options.messages, "--messages");
26721
+ customization.messagesEnabled = parseOnOff4(options.messages, "--messages");
25159
26722
  }
25160
26723
  if (options.clientMessages !== void 0) {
25161
- customization.clientMessagesEnabled = parseOnOff2(
26724
+ customization.clientMessagesEnabled = parseOnOff4(
25162
26725
  options.clientMessages,
25163
26726
  "--client-messages"
25164
26727
  );
@@ -25266,7 +26829,7 @@ function registerSiteCommands(program2) {
25266
26829
  throw new Error("Pass exactly one of --on or --off.");
25267
26830
  }
25268
26831
  const active = Boolean(options.on);
25269
- requireConfirm11(options.confirm, active ? "publish the site" : "take the site offline");
26832
+ requireConfirm14(options.confirm, active ? "publish the site" : "take the site offline");
25270
26833
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25271
26834
  const result = await client.activateSite({ active });
25272
26835
  emit(
@@ -25281,7 +26844,7 @@ function registerSiteCommands(program2) {
25281
26844
  "Change the slug \u2014 heavy: re-registers Stripe/Firebase/Vercel hosts and purges caches. 409 = slug taken. Requires --confirm"
25282
26845
  ).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
26846
  const flags = getGlobalFlags(command);
25284
- requireConfirm11(options.confirm, `change the site slug to "${value}"`);
26847
+ requireConfirm14(options.confirm, `change the site slug to "${value}"`);
25285
26848
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25286
26849
  const result = await client.updateSiteSlug({ slug: value.trim() }).catch(withSlugHint);
25287
26850
  emit(flags, result, () => process.stdout.write(`Slug changed to "${value.trim()}".
@@ -25293,7 +26856,7 @@ function registerSiteCommands(program2) {
25293
26856
  ).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
26857
  const flags = getGlobalFlags(command);
25295
26858
  const list = value.trim().toLowerCase() === "none" ? [] : parseDomainList(value, "<domains>");
25296
- requireConfirm11(
26859
+ requireConfirm14(
25297
26860
  options.confirm,
25298
26861
  list.length === 0 ? "remove every custom domain" : `set the custom domains to ${list.join(", ")}`
25299
26862
  );
@@ -25378,7 +26941,7 @@ function registerSiteCommands(program2) {
25378
26941
  "Nothing to update \u2014 pass --terms-content|--terms-url and/or --privacy-content|--privacy-url."
25379
26942
  );
25380
26943
  }
25381
- requireConfirm11(options.confirm, "change the legal documents");
26944
+ requireConfirm14(options.confirm, "change the legal documents");
25382
26945
  const body = {};
25383
26946
  if (options.language !== void 0) body.language = options.language;
25384
26947
  if (terms) body.terms = terms;
@@ -25430,8 +26993,8 @@ function printStorefront(config) {
25430
26993
  }
25431
26994
 
25432
26995
  // src/commands/smsCampaigns/index.ts
25433
- import { readFileSync as readFileSync24 } from "fs";
25434
- var SMS_PROVIDERS = ["twilio", "smsapi"];
26996
+ import { readFileSync as readFileSync26 } from "fs";
26997
+ var SMS_PROVIDERS2 = ["twilio", "smsapi"];
25435
26998
  var RECIPIENT_STATUSES2 = [
25436
26999
  "pending",
25437
27000
  "sent",
@@ -25446,7 +27009,7 @@ function parsePositiveInt15(value, flag) {
25446
27009
  }
25447
27010
  return n;
25448
27011
  }
25449
- function requireConfirm12(confirm, refusal) {
27012
+ function requireConfirm15(confirm, refusal) {
25450
27013
  if (!confirm) throw new Error(refusal);
25451
27014
  }
25452
27015
  function withSchedulerHint4(error) {
@@ -25463,7 +27026,7 @@ function resolveBody(options) {
25463
27026
  if (options.body !== void 0 && options.bodyFile !== void 0) {
25464
27027
  throw new Error("Use either --body or --body-file, not both.");
25465
27028
  }
25466
- return options.bodyFile !== void 0 ? readFileSync24(options.bodyFile, "utf8") : options.body;
27029
+ return options.bodyFile !== void 0 ? readFileSync26(options.bodyFile, "utf8") : options.body;
25467
27030
  }
25468
27031
  function resolveAudience2(options) {
25469
27032
  if (options.segment !== void 0 && options.allSubscribed) {
@@ -25482,10 +27045,10 @@ function resolveProvider(options) {
25482
27045
  }
25483
27046
  if (options.defaultProvider) return null;
25484
27047
  if (options.provider === void 0) return void 0;
25485
- const match = SMS_PROVIDERS.find((p) => p === options.provider);
27048
+ const match = SMS_PROVIDERS2.find((p) => p === options.provider);
25486
27049
  if (!match) {
25487
27050
  throw new Error(
25488
- `Invalid --provider: ${options.provider} (expected one of ${SMS_PROVIDERS.join(", ")})`
27051
+ `Invalid --provider: ${options.provider} (expected one of ${SMS_PROVIDERS2.join(", ")})`
25489
27052
  );
25490
27053
  }
25491
27054
  return match;
@@ -25570,7 +27133,7 @@ function registerSmsCampaignsCommands(program2) {
25570
27133
  });
25571
27134
  smsCampaigns.command("create").description(
25572
27135
  "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(
27136
+ ).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
27137
  async (options, command) => {
25575
27138
  const flags = getGlobalFlags(command);
25576
27139
  const body = resolveBody(options);
@@ -25593,7 +27156,7 @@ function registerSmsCampaignsCommands(program2) {
25593
27156
  });
25594
27157
  }
25595
27158
  );
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(
27159
+ 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
27160
  async (id, options, command) => {
25598
27161
  const flags = getGlobalFlags(command);
25599
27162
  const input = {};
@@ -25625,7 +27188,7 @@ function registerSmsCampaignsCommands(program2) {
25625
27188
  "PERMANENTLY delete an SMS campaign \u2014 cannot be undone; draft/cancelled/failed only (sent campaigns are audit records and stay). Requires --confirm"
25626
27189
  ).argument("<id>", "SMS campaign id").option("--confirm", "required \u2014 acknowledge the permanent deletion").action(async (id, options, command) => {
25627
27190
  const flags = getGlobalFlags(command);
25628
- requireConfirm12(
27191
+ requireConfirm15(
25629
27192
  options.confirm,
25630
27193
  `Refusing to delete SMS campaign ${id} without --confirm. Re-run with --confirm to proceed.`
25631
27194
  );
@@ -25643,7 +27206,7 @@ function registerSmsCampaignsCommands(program2) {
25643
27206
  "schedule the send (ISO, max 30 days out) instead of sending now"
25644
27207
  ).option("--confirm", "required \u2014 acknowledge this texts the audience for real").action(async (id, options, command) => {
25645
27208
  const flags = getGlobalFlags(command);
25646
- requireConfirm12(
27209
+ requireConfirm15(
25647
27210
  options.confirm,
25648
27211
  `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
27212
  );
@@ -26272,7 +27835,7 @@ function describeAttempts(item) {
26272
27835
  const next = item.nextPaymentAttempt !== null ? formatEpochSeconds(item.nextPaymentAttempt) : "";
26273
27836
  return next ? `${attempts} (next ${next})` : String(attempts);
26274
27837
  }
26275
- function requireConfirm13(confirm, what) {
27838
+ function requireConfirm16(confirm, what) {
26276
27839
  if (!confirm) {
26277
27840
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
26278
27841
  }
@@ -26346,7 +27909,7 @@ function registerSubscriptionsCommands(program2) {
26346
27909
  "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
27910
  ).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
27911
  const flags = getGlobalFlags(command);
26349
- requireConfirm13(options.confirm, `retry the charge for invoice ${invoiceId} (real charge)`);
27912
+ requireConfirm16(options.confirm, `retry the charge for invoice ${invoiceId} (real charge)`);
26350
27913
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26351
27914
  const result = await client.retrySubscriptionPayment(invoiceId);
26352
27915
  emit(flags, result, () => {
@@ -26883,7 +28446,7 @@ Next page: --cursor ${page.nextCursor}
26883
28446
  }
26884
28447
 
26885
28448
  // src/commands/workflows/index.ts
26886
- import { readFileSync as readFileSync25 } from "fs";
28449
+ import { readFileSync as readFileSync27 } from "fs";
26887
28450
 
26888
28451
  // src/commands/workflows/authoring.ts
26889
28452
  import { writeFileSync as writeFileSync6 } from "fs";
@@ -27049,14 +28612,14 @@ function parsePositiveInt18(value, flag) {
27049
28612
  }
27050
28613
  return n;
27051
28614
  }
27052
- function requireConfirm14(confirm, what) {
28615
+ function requireConfirm17(confirm, what) {
27053
28616
  if (!confirm) {
27054
28617
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
27055
28618
  }
27056
28619
  }
27057
28620
  function parseTriggerContext(value) {
27058
28621
  const trimmed = value.trim();
27059
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync25(value, "utf8");
28622
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync27(value, "utf8");
27060
28623
  let parsed;
27061
28624
  try {
27062
28625
  parsed = JSON.parse(raw);
@@ -27102,42 +28665,26 @@ function registerWorkflowsCommands(program2) {
27102
28665
  );
27103
28666
  });
27104
28667
  });
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) => {
28668
+ workflows.command("get").description(
28669
+ "Show a single workflow (triggers, product scope, node counts, last run); --graph walks every step incl. the emails it sends"
28670
+ ).argument("<automationId>", "workflow automation id").option(
28671
+ "--graph",
28672
+ "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."
28673
+ ).action(async (automationId, options, command) => {
27106
28674
  const flags = getGlobalFlags(command);
27107
28675
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27108
- const workflow = await client.getAutomation(automationId);
28676
+ if (!options.graph) {
28677
+ const workflow2 = await client.getAutomation(automationId);
28678
+ emit(flags, workflow2, () => printWorkflowSummary(workflow2));
28679
+ return;
28680
+ }
28681
+ const workflow = await client.getAutomationGraph(automationId);
28682
+ const published = asAutomationGraph(workflow.publishedGraph);
28683
+ const draft = asAutomationGraph(workflow.draftGraph);
28684
+ const templateNames = await resolveTemplateNames(client, [published, draft]);
27109
28685
  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
- `);
28686
+ printWorkflowSummary(workflow);
28687
+ printWorkflowGraphs(workflow, published, draft, templateNames);
27141
28688
  });
27142
28689
  });
27143
28690
  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 +28709,17 @@ function registerWorkflowsCommands(program2) {
27162
28709
  );
27163
28710
  });
27164
28711
  });
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) => {
28712
+ workflows.command("pause").description(
28713
+ "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)"
28714
+ ).argument("<automationId>", "workflow automation id").action(async (automationId, _, command) => {
27166
28715
  const flags = getGlobalFlags(command);
27167
28716
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27168
28717
  const result = await client.setAutomationStatus(automationId, "paused");
27169
28718
  emit(flags, result, () => {
27170
- process.stdout.write(`Paused workflow ${result.id}.
27171
- `);
28719
+ process.stdout.write(
28720
+ `Paused workflow ${result.id} (${result.cancelledRuns} in-flight run(s) cancelled).
28721
+ `
28722
+ );
27172
28723
  });
27173
28724
  });
27174
28725
  workflows.command("resume").description(
@@ -27187,7 +28738,7 @@ function registerWorkflowsCommands(program2) {
27187
28738
  ).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
28739
  async (automationId, runId, options, command) => {
27189
28740
  const flags = getGlobalFlags(command);
27190
- requireConfirm14(options.confirm, `retry run ${runId} (its actions execute again)`);
28741
+ requireConfirm17(options.confirm, `retry run ${runId} (its actions execute again)`);
27191
28742
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27192
28743
  const result = await client.retryAutomationRun(automationId, runId).catch(withSchedulerHint5);
27193
28744
  emit(flags, result, () => {
@@ -27211,7 +28762,7 @@ Follow it with: zanfia workflows runs ${automationId}
27211
28762
  async (automationId, options, command) => {
27212
28763
  const flags = getGlobalFlags(command);
27213
28764
  const context = parseTriggerContext(options.context);
27214
- requireConfirm14(options.confirm, `test-run workflow ${automationId} (real actions fire)`);
28765
+ requireConfirm17(options.confirm, `test-run workflow ${automationId} (real actions fire)`);
27215
28766
  process.stderr.write(
27216
28767
  "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
28768
  );
@@ -27240,9 +28791,352 @@ function formatSteps(steps) {
27240
28791
  if (steps.skipped > 0) parts.push(`${steps.skipped} skipped`);
27241
28792
  return parts.join(", ");
27242
28793
  }
28794
+ function printWorkflowSummary(workflow) {
28795
+ process.stdout.write(`Workflow ${workflow.name} (${workflow.id})
28796
+ `);
28797
+ if (workflow.description) process.stdout.write(` Description: ${workflow.description}
28798
+ `);
28799
+ process.stdout.write(` Status: ${workflow.status}
28800
+ `);
28801
+ process.stdout.write(` Triggers: ${workflow.triggers.join(", ")}
28802
+ `);
28803
+ process.stdout.write(
28804
+ ` Products: ${workflow.productIds.length > 0 ? workflow.productIds.join(", ") : "all"}
28805
+ `
28806
+ );
28807
+ process.stdout.write(
28808
+ ` Published: ${workflow.published ? `yes${workflow.publishedAt ? ` (${workflow.publishedAt})` : ""}` : "no (draft \u2014 cannot run until published from the dashboard)"}
28809
+ `
28810
+ );
28811
+ const { nodeCounts } = workflow;
28812
+ process.stdout.write(
28813
+ ` Nodes: ${nodeCounts.triggers} triggers, ${nodeCounts.actions} actions, ${nodeCounts.conditions} conditions, ${nodeCounts.delays} delays
28814
+ `
28815
+ );
28816
+ process.stdout.write(` Runs this month: ${workflow.runsThisMonth}
28817
+ `);
28818
+ if (workflow.lastRunAt) {
28819
+ process.stdout.write(
28820
+ ` Last run: ${workflow.lastRunAt}${workflow.lastRunStatus ? ` (${workflow.lastRunStatus})` : ""}
28821
+ `
28822
+ );
28823
+ }
28824
+ if (workflow.createdAt) process.stdout.write(` Created: ${workflow.createdAt}
28825
+ `);
28826
+ }
28827
+ var GRAPH_ACTION_LABELS = {
28828
+ sendEmail: "Send email",
28829
+ notifyOwner: "Notify owner (email)",
28830
+ sendSms: "Send SMS",
28831
+ webhook: "Webhook",
28832
+ publishSocialPost: "Publish social post",
28833
+ createCommunityPost: "Create community post",
28834
+ updateClientTags: "Update client tags",
28835
+ setEmailSubscription: "Set email subscription",
28836
+ requestTestimonial: "Request testimonial"
28837
+ };
28838
+ var GRAPH_NODE_KINDS = /* @__PURE__ */ new Set([
28839
+ "trigger",
28840
+ "action",
28841
+ "condition",
28842
+ "delay"
28843
+ ]);
28844
+ var GRAPH_CONDITION_BRANCHES = ["true", "false"];
28845
+ var EXCERPT_LENGTH = 120;
28846
+ function isRecord2(value) {
28847
+ return value !== null && typeof value === "object" && !Array.isArray(value);
28848
+ }
28849
+ function optionalString(value) {
28850
+ return typeof value === "string" ? value : void 0;
28851
+ }
28852
+ function asAutomationGraph(raw) {
28853
+ if (!isRecord2(raw) || !Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) return null;
28854
+ const nodes = [];
28855
+ for (const candidate of raw.nodes) {
28856
+ if (!isRecord2(candidate)) continue;
28857
+ const { id, kind, config, name, disabled } = candidate;
28858
+ if (typeof id !== "string" || typeof kind !== "string" || !GRAPH_NODE_KINDS.has(kind)) {
28859
+ continue;
28860
+ }
28861
+ nodes.push({
28862
+ id,
28863
+ kind,
28864
+ config: isRecord2(config) ? config : {},
28865
+ ...typeof name === "string" && name.length > 0 ? { name } : {},
28866
+ ...disabled === true ? { disabled: true } : {}
28867
+ });
28868
+ }
28869
+ const edges = [];
28870
+ for (const candidate of raw.edges) {
28871
+ if (!isRecord2(candidate)) continue;
28872
+ const { source, target, sourceHandle } = candidate;
28873
+ if (typeof source !== "string" || typeof target !== "string") continue;
28874
+ const branch = GRAPH_CONDITION_BRANCHES.find((b) => b === sourceHandle);
28875
+ edges.push({ source, target, ...branch !== void 0 ? { sourceHandle: branch } : {} });
28876
+ }
28877
+ return { nodes, edges };
28878
+ }
28879
+ function emailConfigOf(node) {
28880
+ const raw = node.config.email;
28881
+ if (!isRecord2(raw)) return null;
28882
+ return {
28883
+ templateId: optionalString(raw.templateId) ?? null,
28884
+ templateName: optionalString(raw.templateName) ?? null,
28885
+ subject: optionalString(raw.subject),
28886
+ html: optionalString(raw.html)
28887
+ };
28888
+ }
28889
+ async function resolveTemplateNames(client, graphs) {
28890
+ const ids = /* @__PURE__ */ new Set();
28891
+ for (const graph of graphs) {
28892
+ for (const node of graph?.nodes ?? []) {
28893
+ const templateId = emailConfigOf(node)?.templateId;
28894
+ if (templateId) ids.add(templateId);
28895
+ }
28896
+ }
28897
+ const names = /* @__PURE__ */ new Map();
28898
+ await Promise.all(
28899
+ [...ids].map(async (id) => {
28900
+ try {
28901
+ const { email } = await client.getLibraryEmail(id);
28902
+ names.set(id, email.name);
28903
+ } catch {
28904
+ }
28905
+ })
28906
+ );
28907
+ return names;
28908
+ }
28909
+ function printWorkflowGraphs(workflow, published, draft, templateNames) {
28910
+ if (workflow.draftGraph === void 0 && workflow.publishedGraph === void 0) {
28911
+ process.stdout.write(
28912
+ "\nThis API does not return workflow graphs yet (it predates `?include=graph`) \u2014 update the API to see the steps.\n"
28913
+ );
28914
+ return;
28915
+ }
28916
+ const differs = workflow.hasUnpublishedChanges === true;
28917
+ if (published) {
28918
+ process.stdout.write(
28919
+ `
28920
+ Graph shown: published (what runs now)${differs ? " \u2014 the draft has unpublished changes, listed below" : ""}
28921
+ `
28922
+ );
28923
+ printGraphWalk(published, templateNames);
28924
+ if (differs && draft) {
28925
+ process.stdout.write("\nDraft graph (unpublished changes \u2014 not running):\n");
28926
+ printGraphWalk(draft, templateNames);
28927
+ }
28928
+ return;
28929
+ }
28930
+ if (draft) {
28931
+ process.stdout.write("\nGraph shown: draft (never published \u2014 nothing runs yet)\n");
28932
+ printGraphWalk(draft, templateNames);
28933
+ return;
28934
+ }
28935
+ process.stdout.write("\n(no graph)\n");
28936
+ }
28937
+ function printGraphWalk(graph, templateNames) {
28938
+ const lines = walkGraph(graph, templateNames);
28939
+ if (lines.length === 0) {
28940
+ process.stdout.write(" (empty graph)\n");
28941
+ return;
28942
+ }
28943
+ for (const line of lines) process.stdout.write(line.length > 0 ? ` ${line}
28944
+ ` : "\n");
28945
+ }
28946
+ function walkGraph(graph, templateNames) {
28947
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
28948
+ const outgoing = /* @__PURE__ */ new Map();
28949
+ for (const edge of graph.edges) {
28950
+ const list = outgoing.get(edge.source) ?? [];
28951
+ list.push(edge);
28952
+ outgoing.set(edge.source, list);
28953
+ }
28954
+ const lines = [];
28955
+ const reached = /* @__PURE__ */ new Set();
28956
+ const visit = (nodeId, depth, path2) => {
28957
+ const node = nodesById.get(nodeId);
28958
+ const indent = " ".repeat(depth);
28959
+ if (!node) {
28960
+ lines.push(`${indent}-> (missing node ${nodeId})`);
28961
+ return;
28962
+ }
28963
+ if (path2.has(nodeId)) {
28964
+ lines.push(`${indent}-> loops back to ${describeNodeRef(node)}`);
28965
+ return;
28966
+ }
28967
+ reached.add(nodeId);
28968
+ lines.push(`${indent}${describeNode(node, templateNames)}`);
28969
+ const nextPath = new Set(path2).add(nodeId);
28970
+ const edges = outgoing.get(nodeId) ?? [];
28971
+ if (node.kind === "condition") {
28972
+ for (const branch of GRAPH_CONDITION_BRANCHES) {
28973
+ const branchEdges = edges.filter((edge) => edge.sourceHandle === branch);
28974
+ lines.push(
28975
+ `${indent} ${branch === "true" ? "yes" : "no"} ->${branchEdges.length === 0 ? " (end)" : ""}`
28976
+ );
28977
+ for (const edge of branchEdges) visit(edge.target, depth + 2, nextPath);
28978
+ }
28979
+ return;
28980
+ }
28981
+ for (const edge of edges) visit(edge.target, depth + (edges.length > 1 ? 1 : 0), nextPath);
28982
+ };
28983
+ const triggers = graph.nodes.filter((node) => node.kind === "trigger");
28984
+ triggers.forEach((trigger, index) => {
28985
+ if (index > 0) lines.push("");
28986
+ visit(trigger.id, 0, /* @__PURE__ */ new Set());
28987
+ });
28988
+ const orphans = graph.nodes.filter((node) => !reached.has(node.id));
28989
+ if (orphans.length > 0) {
28990
+ if (lines.length > 0) lines.push("");
28991
+ lines.push("Not reachable from any trigger (never runs):");
28992
+ for (const node of orphans) lines.push(` ${describeNode(node, templateNames)}`);
28993
+ }
28994
+ return lines;
28995
+ }
28996
+ function describeNodeRef(node) {
28997
+ return node.name ? `"${node.name}" (${node.id})` : node.id;
28998
+ }
28999
+ function describeNode(node, templateNames) {
29000
+ const prefix = `[${node.kind}${node.disabled ? ", disabled" : ""}]`;
29001
+ const label = node.name ? ` "${node.name}"` : "";
29002
+ const suffix = ` (${node.id})`;
29003
+ switch (node.kind) {
29004
+ case "trigger":
29005
+ return `${prefix}${label} ${describeTrigger(node.config)}${suffix}`;
29006
+ case "action":
29007
+ return `${prefix}${label} ${describeAction(node, templateNames)}${suffix}`;
29008
+ case "condition":
29009
+ return `${prefix}${label} if ${describeCondition2(node.config)}${suffix}`;
29010
+ case "delay":
29011
+ return `${prefix}${label} ${describeDelay(node.config)}${suffix}`;
29012
+ }
29013
+ }
29014
+ function describeTrigger(config) {
29015
+ const eventType = optionalString(config.eventType) ?? "(no event type)";
29016
+ const scopes = [];
29017
+ const scopeOf2 = (key, label) => {
29018
+ const ids = config[key];
29019
+ if (Array.isArray(ids) && ids.length > 0) scopes.push(`${label}: ${ids.join(", ")}`);
29020
+ };
29021
+ scopeOf2("productIds", "products");
29022
+ scopeOf2("formIds", "forms");
29023
+ scopeOf2("tagIds", "tags");
29024
+ if (typeof config.inactiveDays === "number") scopes.push(`inactive ${config.inactiveDays} days`);
29025
+ return scopes.length > 0 ? `${eventType} \u2014 ${scopes.join("; ")}` : eventType;
29026
+ }
29027
+ function isKnownActionType(value) {
29028
+ return typeof value === "string" && value in GRAPH_ACTION_LABELS;
29029
+ }
29030
+ function describeAction(node, templateNames) {
29031
+ const { taskType } = node.config;
29032
+ if (!isKnownActionType(taskType)) {
29033
+ const inner = isRecord2(node.config.action) ? optionalString(node.config.action.type) : void 0;
29034
+ return `${typeof taskType === "string" ? taskType : "action"}${inner ? ` ${inner}` : ""}`;
29035
+ }
29036
+ const label = GRAPH_ACTION_LABELS[taskType];
29037
+ switch (taskType) {
29038
+ case "sendEmail":
29039
+ return `${label} \u2014 ${describeEmail(node, templateNames)}`;
29040
+ case "notifyOwner": {
29041
+ const owner = isRecord2(node.config.ownerNotification) ? node.config.ownerNotification : {};
29042
+ return `${label} \u2014 subject: ${quote(optionalString(owner.subject))} \u2014 body: ${quote(excerptHtml(optionalString(owner.html)))}`;
29043
+ }
29044
+ case "sendSms": {
29045
+ const sms = isRecord2(node.config.sms) ? node.config.sms : {};
29046
+ return `${label} \u2014 text: ${quote(excerptText(optionalString(sms.text)))}`;
29047
+ }
29048
+ case "webhook": {
29049
+ const webhook = isRecord2(node.config.webhook) ? node.config.webhook : {};
29050
+ const method = optionalString(webhook.method) ?? optionalString(webhook.type);
29051
+ return `${label} \u2014 ${method ? `${method.toUpperCase()} ` : ""}${optionalString(webhook.url) ?? "(no url)"}`;
29052
+ }
29053
+ case "publishSocialPost": {
29054
+ const social = isRecord2(node.config.social) ? node.config.social : {};
29055
+ const targets = Array.isArray(social.targets) ? social.targets.join(", ") : "";
29056
+ return `${label}${optionalString(social.mode) ? ` (${social.mode})` : ""}${targets ? ` to ${targets}` : ""} \u2014 text: ${quote(excerptText(optionalString(social.text)))}`;
29057
+ }
29058
+ case "createCommunityPost": {
29059
+ const post = isRecord2(node.config.communityPost) ? node.config.communityPost : {};
29060
+ const title = optionalString(post.title);
29061
+ return `${label} in channel ${optionalString(post.channelId) ?? "?"}${title ? ` \u2014 title: ${quote(title)}` : ""} \u2014 text: ${quote(excerptText(optionalString(post.text)))}`;
29062
+ }
29063
+ case "updateClientTags": {
29064
+ const tags = isRecord2(node.config.clientTags) ? node.config.clientTags : {};
29065
+ const ids = Array.isArray(tags.tagIds) ? tags.tagIds.join(", ") : "";
29066
+ return `${label} \u2014 ${optionalString(tags.mode) ?? "update"} ${ids || "(no tags)"}`;
29067
+ }
29068
+ case "setEmailSubscription": {
29069
+ const sub = isRecord2(node.config.emailSubscription) ? node.config.emailSubscription : {};
29070
+ return `${label} \u2014 ${optionalString(sub.status) ?? "?"}`;
29071
+ }
29072
+ case "requestTestimonial":
29073
+ return label;
29074
+ }
29075
+ }
29076
+ function describeEmail(node, templateNames) {
29077
+ const email = emailConfigOf(node);
29078
+ if (!email) return "(no email config)";
29079
+ if (email.templateId) {
29080
+ const name = templateNames.get(email.templateId) ?? email.templateName ?? null;
29081
+ return `library email ${name ? `${quote(name)} ` : ""}(${email.templateId}) \u2014 sent as saved in the library`;
29082
+ }
29083
+ return `subject: ${quote(email.subject)} \u2014 body: ${quote(excerptHtml(email.html))}`;
29084
+ }
29085
+ function describeCondition2(config) {
29086
+ const rules = Array.isArray(config.rules) ? config.rules.filter(isRecord2) : [];
29087
+ if (rules.length === 0) return "(no rules)";
29088
+ return rules.map(describeRule).join(" AND ");
29089
+ }
29090
+ function describeRule(rule) {
29091
+ const field = optionalString(rule.field) ?? "?";
29092
+ const details = [];
29093
+ for (const key of ["productId", "tagId", "consentName", "days"]) {
29094
+ const value = rule[key];
29095
+ if (typeof value === "string" || typeof value === "number") details.push(`${key}=${value}`);
29096
+ }
29097
+ const detail = details.length > 0 ? ` (${details.join(", ")})` : "";
29098
+ const op = optionalString(rule.op);
29099
+ if (op !== void 0) {
29100
+ const value = Array.isArray(rule.values) ? `[${rule.values.join(", ")}]` : typeof rule.value === "string" || typeof rule.value === "number" ? String(rule.value) : "?";
29101
+ return `${field} ${op} ${value}${detail}`;
29102
+ }
29103
+ if (typeof rule.expected === "boolean") return `${field}${detail} is ${rule.expected}`;
29104
+ return `${field}${detail}`;
29105
+ }
29106
+ function describeDelay(config) {
29107
+ const mode = optionalString(config.mode);
29108
+ switch (mode) {
29109
+ case void 0:
29110
+ case "duration":
29111
+ return `wait ${typeof config.amount === "number" ? config.amount : "?"} ${optionalString(config.unit) ?? ""}`.trimEnd();
29112
+ case "untilWeekday":
29113
+ return `wait until weekday ${typeof config.weekday === "number" ? config.weekday : "?"} at ${optionalString(config.time) ?? "?"}${optionalString(config.timezone) ? ` (${config.timezone})` : ""}`;
29114
+ case "untilDate":
29115
+ return `wait until ${optionalString(config.date) ?? "?"}${optionalString(config.timezone) ? ` (${config.timezone})` : ""}`;
29116
+ case "untilContextDate": {
29117
+ const offset = typeof config.offsetMinutes === "number" ? config.offsetMinutes : 0;
29118
+ return `wait until ${optionalString(config.contextKey) ?? "?"}${offset !== 0 ? ` ${offset > 0 ? "+" : ""}${offset} min` : ""}${config.skipWhenPast === true ? " (skip branch when past)" : ""}`;
29119
+ }
29120
+ default:
29121
+ return `wait (${mode})`;
29122
+ }
29123
+ }
29124
+ function quote(value) {
29125
+ return value !== void 0 && value.length > 0 ? `"${value}"` : "(empty)";
29126
+ }
29127
+ function excerptHtml(html) {
29128
+ if (html === void 0) return void 0;
29129
+ 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, "'");
29130
+ return excerptText(text);
29131
+ }
29132
+ function excerptText(text) {
29133
+ if (text === void 0) return void 0;
29134
+ const collapsed = text.replace(/\s+/g, " ").trim();
29135
+ return collapsed.length > EXCERPT_LENGTH ? `${collapsed.slice(0, EXCERPT_LENGTH - 1)}\u2026` : collapsed;
29136
+ }
27243
29137
 
27244
29138
  // src/commands/workspace/index.ts
27245
- function parseOnOff3(value, flag) {
29139
+ function parseOnOff5(value, flag) {
27246
29140
  const match = ["on", "off"].find((v) => v === value);
27247
29141
  if (!match) throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
27248
29142
  return match === "on";
@@ -27310,7 +29204,7 @@ function registerWorkspaceCommands(program2) {
27310
29204
  }
27311
29205
  if (options.inheritTimezone) input.timezone = null;
27312
29206
  if (options.autoTranscribe !== void 0) {
27313
- input.autoTranscribeUploads = parseOnOff3(options.autoTranscribe, "--auto-transcribe");
29207
+ input.autoTranscribeUploads = parseOnOff5(options.autoTranscribe, "--auto-transcribe");
27314
29208
  }
27315
29209
  if (options.transcriptionLanguage !== void 0) {
27316
29210
  input.transcriptionLanguage = parseTranscriptionLanguage(options.transcriptionLanguage);
@@ -27327,13 +29221,65 @@ function registerWorkspaceCommands(program2) {
27327
29221
  );
27328
29222
  }
27329
29223
 
29224
+ // src/utils/shortHelp.ts
29225
+ var SHORT_HELP_FLAG = "--short";
29226
+ var SHORT_HELP_DESCRIPTION = "with --help: print only the command names (subcommands indented), no descriptions";
29227
+ var INDENT = " ";
29228
+ function rootOf(command) {
29229
+ let current = command;
29230
+ while (current.parent) current = current.parent;
29231
+ return current;
29232
+ }
29233
+ function rawArgsOf(command) {
29234
+ const root = rootOf(command);
29235
+ if ("rawArgs" in root && Array.isArray(root.rawArgs)) {
29236
+ return root.rawArgs.filter((arg) => typeof arg === "string");
29237
+ }
29238
+ return [];
29239
+ }
29240
+ function isShortHelpRequested(command) {
29241
+ return rawArgsOf(command).includes(SHORT_HELP_FLAG);
29242
+ }
29243
+ function visibleSubcommands(command, helper) {
29244
+ return helper.visibleCommands(command).filter((sub) => command.commands.includes(sub));
29245
+ }
29246
+ function formatShortHelp(command, helper) {
29247
+ const lines = [];
29248
+ const walk = (cmd, depth) => {
29249
+ lines.push(`${INDENT.repeat(depth)}${cmd.name()}`);
29250
+ for (const sub of visibleSubcommands(cmd, helper)) walk(sub, depth + 1);
29251
+ };
29252
+ if (command.parent === null) {
29253
+ for (const sub of visibleSubcommands(command, helper)) walk(sub, 0);
29254
+ } else {
29255
+ walk(command, 0);
29256
+ }
29257
+ return `${lines.join("\n")}
29258
+ `;
29259
+ }
29260
+ function registerOnCommand2(command) {
29261
+ if (!command.options.some((option) => option.long === SHORT_HELP_FLAG)) {
29262
+ command.addOption(new Option(SHORT_HELP_FLAG, SHORT_HELP_DESCRIPTION).hideHelp());
29263
+ }
29264
+ const inherited = command.configureHelp();
29265
+ const previousFormatHelp = inherited.formatHelp;
29266
+ command.configureHelp({
29267
+ ...inherited,
29268
+ formatHelp: (cmd, helper) => isShortHelpRequested(cmd) ? formatShortHelp(cmd, helper) : previousFormatHelp ? previousFormatHelp.call(helper, cmd, helper) : Help.prototype.formatHelp.call(helper, cmd, helper)
29269
+ });
29270
+ for (const sub of command.commands) registerOnCommand2(sub);
29271
+ }
29272
+ function applyShortHelp(program2) {
29273
+ registerOnCommand2(program2);
29274
+ }
29275
+
27330
29276
  // src/program.ts
27331
29277
  function resolveCliVersion() {
27332
- if ("0.3.47".length > 0) {
27333
- return "0.3.47";
29278
+ if ("0.3.52".length > 0) {
29279
+ return "0.3.52";
27334
29280
  }
27335
29281
  try {
27336
- const raw = readFileSync26(new URL("../package.json", import.meta.url), "utf8");
29282
+ const raw = readFileSync28(new URL("../package.json", import.meta.url), "utf8");
27337
29283
  const parsed = JSON.parse(raw);
27338
29284
  if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
27339
29285
  const { version: version2 } = parsed;
@@ -27376,6 +29322,7 @@ Signing in \u2014 two roles, two consent pages:
27376
29322
  registerProductsCommands(program2);
27377
29323
  registerOffersCommands(program2);
27378
29324
  registerCheckoutSettingsCommands(program2);
29325
+ registerIntegrationsCommands(program2);
27379
29326
  registerSiteCommands(program2);
27380
29327
  registerEmailsCommands(program2);
27381
29328
  registerOtoCommands(program2);
@@ -27406,6 +29353,7 @@ Signing in \u2014 two roles, two consent pages:
27406
29353
  registerSocialCommands(program2);
27407
29354
  registerMediaCommands(program2);
27408
29355
  applyGlobalOptions(program2);
29356
+ applyShortHelp(program2);
27409
29357
  return program2;
27410
29358
  }
27411
29359