@zanfia/cli 0.6.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +541 -64
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5249,9 +5249,9 @@ var require_lodash7 = __commonJS({
5249
5249
  }
5250
5250
  });
5251
5251
 
5252
- // ../../node_modules/combine-errors/node_modules/lodash.uniqby/index.js
5252
+ // ../../node_modules/lodash.uniqby/index.js
5253
5253
  var require_lodash8 = __commonJS({
5254
- "../../node_modules/combine-errors/node_modules/lodash.uniqby/index.js"(exports, module) {
5254
+ "../../node_modules/lodash.uniqby/index.js"(exports, module) {
5255
5255
  "use strict";
5256
5256
  var baseIteratee = require_lodash4();
5257
5257
  var baseUniq = require_lodash7();
@@ -7380,7 +7380,8 @@ var mockBackend = {
7380
7380
  status: "draft",
7381
7381
  isFree: false,
7382
7382
  createdAt: now(),
7383
- clientCount: 0
7383
+ clientCount: 0,
7384
+ language: input.language ?? null
7384
7385
  };
7385
7386
  products.push(product);
7386
7387
  return toSummary(product);
@@ -7390,6 +7391,7 @@ var mockBackend = {
7390
7391
  if (!product) return null;
7391
7392
  if (input.name !== void 0) product.name = input.name;
7392
7393
  if (input.description !== void 0) product.description = input.description;
7394
+ if (input.language !== void 0) product.language = input.language;
7393
7395
  return { productId: id };
7394
7396
  },
7395
7397
  archiveProduct(id) {
@@ -7897,6 +7899,9 @@ function createMockClient() {
7897
7899
  async listClientTags() {
7898
7900
  return { count: 0, tags: [] };
7899
7901
  },
7902
+ async listClientCustomFields() {
7903
+ return { count: 0, fields: [] };
7904
+ },
7900
7905
  async createClientTag() {
7901
7906
  throw new ApiError(501, "client tag writes are not available in mock mode");
7902
7907
  },
@@ -8119,6 +8124,7 @@ function createMockClient() {
8119
8124
  przelewy24: false,
8120
8125
  paypal: false,
8121
8126
  dodo: false,
8127
+ tubapay: false,
8122
8128
  manual: null
8123
8129
  },
8124
8130
  sms: { defaultProvider: null, twilio: null, smsapi: null }
@@ -9248,6 +9254,7 @@ function createHttpClient(config) {
9248
9254
  ),
9249
9255
  changeClientEmail: (id, newEmail) => request("POST", `/clients/${encodeURIComponent(id)}/change-email`, { newEmail }),
9250
9256
  listClientTags: () => request("GET", "/clients/tags"),
9257
+ listClientCustomFields: (includeArchived) => request("GET", `/clients/custom-fields${includeArchived ? "?includeArchived=true" : ""}`),
9251
9258
  createClientTag: (input) => request("POST", "/clients/tags", input),
9252
9259
  updateClientTag: (tagId, input) => request("PATCH", `/clients/tags/${encodeURIComponent(tagId)}`, input),
9253
9260
  deleteClientTag: (tagId) => request("DELETE", `/clients/tags/${encodeURIComponent(tagId)}`),
@@ -10157,6 +10164,27 @@ function parseStorefrontVisibility(value) {
10157
10164
  }
10158
10165
  return visibility;
10159
10166
  }
10167
+ var PRODUCT_LANGUAGES = ["pl", "en"];
10168
+ var PRODUCT_LANGUAGE_NONE = "none";
10169
+ function parseProductLanguage(value) {
10170
+ const language = PRODUCT_LANGUAGES.find((candidate) => candidate === value.trim().toLowerCase());
10171
+ if (language === void 0) {
10172
+ throw new Error(
10173
+ `Invalid --language: ${value} (expected one of: ${PRODUCT_LANGUAGES.join(", ")})`
10174
+ );
10175
+ }
10176
+ return language;
10177
+ }
10178
+ function parseProductLanguageOrNone(value) {
10179
+ if (value.trim().toLowerCase() === PRODUCT_LANGUAGE_NONE) return PRODUCT_LANGUAGE_NONE;
10180
+ const language = PRODUCT_LANGUAGES.find((candidate) => candidate === value.trim().toLowerCase());
10181
+ if (language === void 0) {
10182
+ throw new Error(
10183
+ `Invalid --language: ${value} (expected one of: ${PRODUCT_LANGUAGES.join(", ")}, ${PRODUCT_LANGUAGE_NONE})`
10184
+ );
10185
+ }
10186
+ return language;
10187
+ }
10160
10188
  function parseCommaList(flag) {
10161
10189
  return (value) => {
10162
10190
  const items = [
@@ -10324,38 +10352,39 @@ var listCertificateTemplatesColumns = ["id", "name", "version", "isDefault", "up
10324
10352
  function registerCertificatesCommands(program2) {
10325
10353
  const certificates = program2.command("certificates");
10326
10354
  certificates.description(
10327
- "Course-completion certificates \u2014 issue for course clients, revoke/reinstate, resend the email, and manage designer templates (`certificates templates \u2026`)"
10355
+ "Course-completion and event-participation certificates \u2014 issue for course or event clients, revoke/reinstate, resend the email, and manage designer templates (`certificates templates \u2026`)"
10328
10356
  );
10329
10357
  certificates.command("list").summary("List certificates").description(
10330
- "Issued course-completion certificates of the workspace, newest first, optionally filtered to one course product (`productId`); `limit` caps the rows (service default applies). Each row is the credential: public `certificateId` (`cert_\u2026`, the id every certificate write takes), recipient name/email, course and issuer names, completion/issued dates, `issuedBy` (manual vs automatic), template id/version, PDF and verification URLs, `status` (active/revoked), `expiresAt` and delivery/engagement signals (email status, views, downloads, LinkedIn add). Also the polling endpoint after issue-certificates \u2014 a newly enqueued client shows up here once its PDF has been rendered."
10331
- ).option("--product <value>", "Course product id \u2014 omit for all courses.").option(
10358
+ "Issued certificates of the workspace (course completion and event participation), newest first, optionally filtered to one product (`productId`); `limit` caps the rows per page (service default applies) and `nextCursor` (pass it back as `cursor`) continues the list. Each row is the credential: public `certificateId` (`cert_\u2026`, the id every certificate write takes), `productType` (`course` / `event`), recipient name/email, product (`courseName`) and issuer names, completion/issued dates, `issuedBy` (manual vs automatic), template id/version, PDF and verification URLs, `status` (active/revoked), `expiresAt` and delivery/engagement signals (email status, views, downloads, LinkedIn add). Also the polling endpoint after issue-certificates \u2014 a newly enqueued client shows up here once its PDF has been rendered."
10359
+ ).option("--product <value>", "Course or event product id \u2014 omit for all products.").option(
10332
10360
  "--limit <n>",
10333
- "Max rows, newest first (max 500; the service default applies when omitted).",
10361
+ "Max rows per page, newest first (max 500; the service default applies when omitted).",
10334
10362
  parseNumberOption("--limit", NumberFormat.Integer)
10335
- ).action(async (options, command) => {
10363
+ ).option("--cursor <value>", "`nextCursor` of the previous page; omit for the first page.").action(async (options, command) => {
10336
10364
  const flags = getGlobalFlags(command);
10337
10365
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10338
10366
  const params = {};
10339
10367
  const payload = {
10340
10368
  productId: options.product,
10341
- limit: options.limit
10369
+ limit: options.limit,
10370
+ cursor: options.cursor
10342
10371
  };
10343
10372
  const result = await client.call("GET", "/certificates", params, payload, void 0);
10344
10373
  const rows = tableRowsOf(result, "certificates");
10345
10374
  emit(flags, result ?? {}, () => printTable(rows, listCertificatesColumns));
10346
10375
  });
10347
10376
  certificates.command("issue").summary("Issue certificates (async)").description(
10348
- 'Issues course-completion certificates to the listed clients of ONE course product. The call validates the batch (404 unknown product or `templateId`, 400 when the product is not a course), skips clients who already hold a certificate for it (one certificate per client per course, ever) and enqueues one issuance task per remaining client \u2014 PDF rendering and the "certificate issued" email happen asynchronously, so the response is 202 with `enqueued` / `skippedExistingCertificate` counts, not the certificates. Poll list-certificates (`productId` filter) a little later for the issued rows. A single issuance is this call with one `clientId`. With `onlyCompleted: true` a client only gets a certificate if their course progress is completed (checked per client at issuance time). `clientIds` is capped at 1000 and de-duplicated. Every issuance EMAILS the student \u2014 the emails cannot be recalled.'
10349
- ).argument("<productId>", "Course product id (must be a course).").requiredOption(
10377
+ 'Issues certificates to the listed clients of ONE product: a course (completion certificate) or an event \u2014 webinar, workshop, also one held outside Zanfia (participation certificate, "for taking part in", no attendance check). The call validates the batch (404 unknown product or `templateId`, 400 when the product is neither a course nor an event), skips clients who already hold a certificate for it (one certificate per client per product, ever) and enqueues one issuance task per remaining client \u2014 PDF rendering and the "certificate issued" email happen asynchronously, so the response is 202 with `enqueued` / `skippedExistingCertificate` counts, not the certificates. Poll list-certificates (`productId` filter) a little later for the issued rows. A single issuance is this call with one `clientId`. With `onlyCompleted: true` a client only gets a certificate if their course progress is completed (checked per client at issuance time; courses only, 400 for an event). `clientIds` is capped at 1000 and de-duplicated. Every issuance EMAILS the student \u2014 the emails cannot be recalled.'
10378
+ ).argument("<productId>", "Course or event product id (other product types answer 400).").requiredOption(
10350
10379
  "--client <a,b,...>",
10351
10380
  "Workspace client ids; one id = single issuance. Max 1000, de-duplicated.",
10352
10381
  parseCommaList("--client")
10353
10382
  ).option(
10354
10383
  "--only-completed",
10355
- "Skip clients who have not completed the course (checked per client at issuance time). Default false = issue to everyone listed."
10384
+ "Course products only (400 for an event): skip clients who have not completed the course (checked per client at issuance time). Default false = issue to everyone listed."
10356
10385
  ).option("--no-only-completed", "Send `onlyCompleted: false` explicitly.").option(
10357
10386
  "--template <value>",
10358
- "Explicit template id; omitted = the course template, else the workspace default."
10387
+ "Explicit template id; omitted = the product template, else the workspace default."
10359
10388
  ).option(
10360
10389
  "--reissue-revoked",
10361
10390
  "Re-issue clients whose current certificate is REVOKED (new document in the current template, new number and verification link, the revoked one stays revoked). ACTIVE certificates are still skipped. Default false = any existing certificate skips the client."
@@ -10593,7 +10622,10 @@ function registerBlogCommands(program2) {
10593
10622
  ).option("--content <value>", "The body as Markdown (headings, lists, links, images by URL).").option(
10594
10623
  "--excerpt <value>",
10595
10624
  "Short plain-text summary (list, RSS, meta description); derived from the body on publish when empty."
10596
- ).option("--cover <value>", "Cover image URL (Media Library).").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option("--seo <json|file>", "seo", parseJsonObjectFileOrInline("--seo")).option("--publish", "Publish right away (needs a non-empty body); default false = draft.").option("--no-publish", "Send `publish: false` explicitly.").action(async (options, command) => {
10625
+ ).option("--cover <value>", "Cover image URL (Media Library).").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option("--seo <json|file>", "seo", parseJsonObjectFileOrInline("--seo")).option("--publish", "Publish right away (needs a non-empty body); default false = draft.").option("--no-publish", "Send `publish: false` explicitly.").option(
10626
+ "--published-at <value>",
10627
+ "The post's date, ISO 8601 date (2025-10-16) or date-time, not in the future - e.g. the original date of an imported post. Omitted = the moment of the first publication."
10628
+ ).action(async (options, command) => {
10597
10629
  const flags = getGlobalFlags(command);
10598
10630
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10599
10631
  const params = {};
@@ -10605,7 +10637,8 @@ function registerBlogCommands(program2) {
10605
10637
  coverImageUrl: options.cover,
10606
10638
  tags: options.tags,
10607
10639
  seo: options.seo,
10608
- publish: options.publish
10640
+ publish: options.publish,
10641
+ publishedAt: options.publishedAt
10609
10642
  };
10610
10643
  const result = await client.call("POST", "/blog-posts", params, void 0, payload);
10611
10644
  emit(flags, result ?? {}, () => printJson(result ?? {}));
@@ -10633,6 +10666,9 @@ function registerBlogCommands(program2) {
10633
10666
  "--seo <json|file>",
10634
10667
  "Replaces the whole SEO section.",
10635
10668
  parseJsonObjectFileOrInline("--seo")
10669
+ ).option(
10670
+ "--published-at <value>",
10671
+ "The post's date (ISO 8601, not in the future); null clears it on a draft - a published post keeps one."
10636
10672
  ).action(async (id, options, command) => {
10637
10673
  const flags = getGlobalFlags(command);
10638
10674
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -10646,7 +10682,8 @@ function registerBlogCommands(program2) {
10646
10682
  excerpt: options.excerpt,
10647
10683
  coverImageUrl: options.cover,
10648
10684
  tags: options.tags,
10649
- seo: options.seo
10685
+ seo: options.seo,
10686
+ publishedAt: options.publishedAt
10650
10687
  };
10651
10688
  const result = await client.call("PATCH", "/blog-posts/:postId", params, void 0, payload);
10652
10689
  emit(flags, result ?? {}, () => printJson(result ?? {}));
@@ -10691,10 +10728,270 @@ function registerBlogCommands(program2) {
10691
10728
  });
10692
10729
  }
10693
10730
 
10731
+ // src/commands/_generated/site-redirects.ts
10732
+ var listSiteRedirectsColumns = ["from", "to", "kind"];
10733
+ function registerSiteRedirectsCommands(program2) {
10734
+ const siteRedirects = program2.command("site-redirects");
10735
+ siteRedirects.description(
10736
+ "The site's 301 redirect map (Your site \u2192 Redirects): old addresses and where they go; checked by the same rules as the dashboard"
10737
+ );
10738
+ siteRedirects.command("list").summary("List site redirects").description(
10739
+ "The site's 301 redirect map (dashboard: Your site \u2192 Redirects): old addresses of a site moved to Zanfia and where they go now, in map order, plus `max` (the cap). A line fires only when nothing on the site answers the address. `kind` = `exact` (one address) or `folder` (`/news/*`, every address under it). The `from` of a line is its id for update-site-redirect and delete-site-redirect."
10740
+ ).action(async (_options, command) => {
10741
+ const flags = getGlobalFlags(command);
10742
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10743
+ const params = {};
10744
+ const result = await client.call("GET", "/site/redirects", params, void 0, void 0);
10745
+ const rows = tableRowsOf(result, "redirects");
10746
+ emit(flags, result ?? {}, () => printTable(rows, listSiteRedirectsColumns));
10747
+ });
10748
+ siteRedirects.command("create").summary("Add a site redirect").description(
10749
+ "Adds one line at the end of the site's 301 map; live at once. Refused (400 `site-redirects-refused`, `issues[]` = index, from, to, issue) exactly like the dashboard save: a malformed address (`invalid-from` / `invalid-to`), the home page or an address Zanfia owns (`/blog`, `/p/\u2026`, `/app`: `reserved-from`), the member area (`portal-from`), a published page (`page-exists`), a `from` the map already has (`duplicate`), a chain or a loop (`chain`), `/*` in `to` without a folder `from` (`rest-without-folder`), over 500 lines (`too-many-entries`). Returns the line as stored."
10750
+ ).argument(
10751
+ "<from>",
10752
+ "The old address on the site: a path (`/o-nas`), a full address on the site (`https://example.com/o-nas/`, stored as its path) or a folder ending with `/*` (`/news/*`, every address under it). Query string and `#\u2026` are dropped; case is kept."
10753
+ ).argument(
10754
+ "<to>",
10755
+ "Where it goes: a path on the site (`/o-mnie`; a folder line may end with `/*` to keep the rest of the old address: `/news/*` \u2192 `/blog/*`) or an absolute `https://` address."
10756
+ ).action(async (from, to, _options, command) => {
10757
+ const flags = getGlobalFlags(command);
10758
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10759
+ const params = {};
10760
+ const payload = {
10761
+ from,
10762
+ to
10763
+ };
10764
+ const result = await client.call("POST", "/site/redirects", params, void 0, payload);
10765
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10766
+ });
10767
+ siteRedirects.command("update").summary("Change a site redirect").description(
10768
+ "Changes one line of the 301 map, picked by its current `from`: `newFrom`, `to` or both; a field not given keeps its value and the line keeps its place. Live at once. 404 `site-redirect-not-found` when no line has that `from`; refused like create-site-redirect (400 `site-redirects-refused` with `issues[]`). Returns the line as stored."
10769
+ ).argument(
10770
+ "<from>",
10771
+ "The line to change, by its current `from` (as list-site-redirects shows it; `/o-nas/` or a full address on the site also match)."
10772
+ ).option(
10773
+ "--new-from <value>",
10774
+ "The old address on the site: a path (`/o-nas`), a full address on the site (`https://example.com/o-nas/`, stored as its path) or a folder ending with `/*` (`/news/*`, every address under it). Query string and `#\u2026` are dropped; case is kept."
10775
+ ).option(
10776
+ "--to <value>",
10777
+ "Where it goes: a path on the site (`/o-mnie`; a folder line may end with `/*` to keep the rest of the old address: `/news/*` \u2192 `/blog/*`) or an absolute `https://` address."
10778
+ ).action(async (from, options, command) => {
10779
+ const flags = getGlobalFlags(command);
10780
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10781
+ const params = {};
10782
+ const payload = {
10783
+ from,
10784
+ newFrom: options.newFrom,
10785
+ to: options.to
10786
+ };
10787
+ const result = await client.call("PATCH", "/site/redirects", params, void 0, payload);
10788
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10789
+ });
10790
+ siteRedirects.command("delete").summary("Remove a site redirect").description(
10791
+ "Removes one line of the 301 map, picked by its `from` (sent in the JSON body). The old address answers 404 again at once (links to it in search results and old posts break). 404 `site-redirect-not-found` when no line has that `from`. Returns the removed line."
10792
+ ).argument(
10793
+ "<from>",
10794
+ "The line to change, by its current `from` (as list-site-redirects shows it; `/o-nas/` or a full address on the site also match)."
10795
+ ).option(
10796
+ "--confirm",
10797
+ "required \u2014 acknowledge that you want to the old address answers 404 again"
10798
+ ).action(async (from, options, command) => {
10799
+ const flags = getGlobalFlags(command);
10800
+ requireConfirm(options.confirm, "the old address answers 404 again");
10801
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10802
+ const params = {};
10803
+ const payload = {
10804
+ from
10805
+ };
10806
+ const result = await client.call("DELETE", "/site/redirects", params, void 0, payload);
10807
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10808
+ });
10809
+ }
10810
+
10811
+ // src/commands/_generated/popups.ts
10812
+ var listPopupsColumns = ["id", "name", "status", "priority", "updatedAt"];
10813
+ function registerPopupsCommands(program2) {
10814
+ const popups = program2.command("popups");
10815
+ popups.description(
10816
+ "Pop-ups on the workspace site (Your site \u2192 Pop-ups): a signup form shown on entry or after scrolling, after the cookie decision, never again after a signup"
10817
+ );
10818
+ popups.command("list").summary("List pop-ups").description(
10819
+ "Every pop-up of the workspace site (drafts, active, paused) in priority order: copy, form content, triggers per device, targeting, frequency and schedule. The id source for every other pop-up tool."
10820
+ ).action(async (_options, command) => {
10821
+ const flags = getGlobalFlags(command);
10822
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10823
+ const params = {};
10824
+ const result = await client.call("GET", "/popups", params, void 0, void 0);
10825
+ const rows = tableRowsOf(result, "popups");
10826
+ emit(flags, result ?? {}, () => printTable(rows, listPopupsColumns));
10827
+ });
10828
+ popups.command("create").summary("Create a pop-up").description(
10829
+ "Creates a pop-up as a DRAFT holding a signup form. Without `content.formId` a new standalone signup form named after the pop-up is created (visible in Forms). Defaults: desktop after 10 s on the page, phones after 40 % scroll as a bottom sheet, again after 7 days, all hosted pages except thank-you / download / member pages, hidden for subscribers. Activate it with set-popup-status once it has a title and a button text. Requires the pop-ups feature on the plan (403 otherwise)."
10830
+ ).requiredOption(
10831
+ "--name <value>",
10832
+ "Internal name (panel only); also the name of a provisioned form."
10833
+ ).option(
10834
+ "--priority <n>",
10835
+ "Lower shows first when several pop-ups match a page; default = after the others.",
10836
+ parseNumberOption("--priority", NumberFormat.Integer)
10837
+ ).addOption(
10838
+ new Option(
10839
+ "--language <pl|en|default>",
10840
+ "Built-in labels (close, errors): `pl`, `en` or `default` (= the page language)."
10841
+ ).choices(["pl", "en", "default"])
10842
+ ).option(
10843
+ "--copy <json|file>",
10844
+ "Headline, text, button and image.",
10845
+ parseJsonObjectFileOrInline("--copy")
10846
+ ).option(
10847
+ "--content <json|file>",
10848
+ "What the pop-up holds: a signup form.",
10849
+ parseJsonObjectFileOrInline("--content")
10850
+ ).option("--design <json|file>", "design", parseJsonObjectFileOrInline("--design")).option(
10851
+ "--trigger <json|file>",
10852
+ "When it opens, per device.",
10853
+ parseJsonObjectFileOrInline("--trigger")
10854
+ ).option(
10855
+ "--targeting <json|file>",
10856
+ "Where and for whom.",
10857
+ parseJsonObjectFileOrInline("--targeting")
10858
+ ).option(
10859
+ "--frequency <json|file>",
10860
+ "How often one visitor sees it; never again after a signup. Default every 7 days.",
10861
+ parseJsonObjectFileOrInline("--frequency")
10862
+ ).option(
10863
+ "--schedule <json|file>",
10864
+ "From / to (applied within a few minutes).",
10865
+ parseJsonObjectFileOrInline("--schedule")
10866
+ ).action(async (options, command) => {
10867
+ const flags = getGlobalFlags(command);
10868
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10869
+ const params = {};
10870
+ const payload = {
10871
+ name: options.name,
10872
+ priority: options.priority,
10873
+ language: options.language,
10874
+ copy: options.copy,
10875
+ content: options.content,
10876
+ design: options.design,
10877
+ trigger: options.trigger,
10878
+ targeting: options.targeting,
10879
+ frequency: options.frequency,
10880
+ schedule: options.schedule
10881
+ };
10882
+ const result = await client.call("POST", "/popups", params, void 0, payload);
10883
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10884
+ });
10885
+ popups.command("get").summary("Pop-up detail").description(
10886
+ "One pop-up in full: status, copy, form content, triggers, targeting, frequency, schedule."
10887
+ ).argument("<id>", "Pop-up id (`popup_\u2026`) from list-popups.").action(async (id, _options, command) => {
10888
+ const flags = getGlobalFlags(command);
10889
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10890
+ const params = {
10891
+ popupId: id
10892
+ };
10893
+ const result = await client.call("GET", "/popups/:popupId", params, void 0, void 0);
10894
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10895
+ });
10896
+ popups.command("update").summary("Update a pop-up").description(
10897
+ "Partial update of a pop-up: each section sent merges into the current one (copy, content, design, targeting, schedule field by field; a trigger rule, the page list and the frequency are replaced whole). An active pop-up must keep its title, button and form (400 popup-incomplete). Live sites pick changes up within a few minutes."
10898
+ ).argument("<id>", "Pop-up id (`popup_\u2026`) from list-popups.").option("--name <value>", "Internal name (panel only).").option(
10899
+ "--priority <n>",
10900
+ "Lower shows first when several pop-ups match a page; default = after the others.",
10901
+ parseNumberOption("--priority", NumberFormat.Integer)
10902
+ ).addOption(
10903
+ new Option(
10904
+ "--language <pl|en|default>",
10905
+ "Built-in labels (close, errors): `pl`, `en` or `default` (= the page language)."
10906
+ ).choices(["pl", "en", "default"])
10907
+ ).option(
10908
+ "--copy <json|file>",
10909
+ "Headline, text, button and image.",
10910
+ parseJsonObjectFileOrInline("--copy")
10911
+ ).option(
10912
+ "--content <json|file>",
10913
+ "What the pop-up holds: a signup form.",
10914
+ parseJsonObjectFileOrInline("--content")
10915
+ ).option("--design <json|file>", "design", parseJsonObjectFileOrInline("--design")).option(
10916
+ "--trigger <json|file>",
10917
+ "When it opens, per device.",
10918
+ parseJsonObjectFileOrInline("--trigger")
10919
+ ).option(
10920
+ "--targeting <json|file>",
10921
+ "Where and for whom.",
10922
+ parseJsonObjectFileOrInline("--targeting")
10923
+ ).option(
10924
+ "--frequency <json|file>",
10925
+ "How often one visitor sees it; never again after a signup. Default every 7 days.",
10926
+ parseJsonObjectFileOrInline("--frequency")
10927
+ ).option(
10928
+ "--schedule <json|file>",
10929
+ "From / to (applied within a few minutes).",
10930
+ parseJsonObjectFileOrInline("--schedule")
10931
+ ).action(async (id, options, command) => {
10932
+ const flags = getGlobalFlags(command);
10933
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10934
+ const params = {
10935
+ popupId: id
10936
+ };
10937
+ const payload = {
10938
+ name: options.name,
10939
+ priority: options.priority,
10940
+ language: options.language,
10941
+ copy: options.copy,
10942
+ content: options.content,
10943
+ design: options.design,
10944
+ trigger: options.trigger,
10945
+ targeting: options.targeting,
10946
+ frequency: options.frequency,
10947
+ schedule: options.schedule
10948
+ };
10949
+ const result = await client.call("PATCH", "/popups/:popupId", params, void 0, payload);
10950
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10951
+ });
10952
+ popups.command("status").summary("Activate or pause a pop-up").description(
10953
+ "Activates or pauses a pop-up. Activation needs a title, a button text and an existing signup form (400 popup-incomplete / 404 form-not-found), the pop-ups feature on the plan (403) and a free slot: at most 20 active pop-ups (409 too-many-active-popups). Visitors see it after their cookie decision, by its trigger, once per page view."
10954
+ ).argument("<id>", "Pop-up id (`popup_\u2026`) from list-popups.").argument(
10955
+ "<status>",
10956
+ "`active` shows it on the site (within a few minutes), `paused` takes it off."
10957
+ ).action(async (id, status, _options, command) => {
10958
+ const flags = getGlobalFlags(command);
10959
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10960
+ const params = {
10961
+ popupId: id
10962
+ };
10963
+ const payload = {
10964
+ status
10965
+ };
10966
+ const result = await client.call(
10967
+ "POST",
10968
+ "/popups/:popupId/status",
10969
+ params,
10970
+ void 0,
10971
+ payload
10972
+ );
10973
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10974
+ });
10975
+ popups.command("delete").summary("Delete a pop-up").description(
10976
+ "Deletes a pop-up for good; an active one leaves the site within a few minutes. Its form and the signups stay. No undo - prefer pausing."
10977
+ ).argument("<id>", "Pop-up id (`popup_\u2026`) from list-popups.").option("--confirm", "required \u2014 acknowledge that you want to deletes the pop-up for good").action(async (id, options, command) => {
10978
+ const flags = getGlobalFlags(command);
10979
+ requireConfirm(options.confirm, "deletes the pop-up for good");
10980
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10981
+ const params = {
10982
+ popupId: id
10983
+ };
10984
+ const result = await client.call("DELETE", "/popups/:popupId", params, void 0, void 0);
10985
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10986
+ });
10987
+ }
10988
+
10694
10989
  // src/commands/_generated/index.ts
10695
10990
  function registerGeneratedCommands(program2) {
10696
10991
  registerCertificatesCommands(program2);
10697
10992
  registerBlogCommands(program2);
10993
+ registerSiteRedirectsCommands(program2);
10994
+ registerPopupsCommands(program2);
10698
10995
  }
10699
10996
 
10700
10997
  // src/commands/ads/index.ts
@@ -13248,6 +13545,11 @@ function buildConsentInput(options) {
13248
13545
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
13249
13546
 
13250
13547
  // src/client/types.ts
13548
+ var GRANT_PAYMENT_METHODS = [
13549
+ "bankTransfer",
13550
+ "cash",
13551
+ "other"
13552
+ ];
13251
13553
  var AIImageAspectRatio = /* @__PURE__ */ ((AIImageAspectRatio2) => {
13252
13554
  AIImageAspectRatio2["Square"] = "1:1";
13253
13555
  AIImageAspectRatio2["Portrait"] = "4:5";
@@ -13292,7 +13594,8 @@ var CREDENTIAL_PAYMENT_GATEWAYS = [
13292
13594
  "przelewy24",
13293
13595
  "paynow",
13294
13596
  "paypal",
13295
- "dodo"
13597
+ "dodo",
13598
+ "tubapay"
13296
13599
  ];
13297
13600
  var COMMUNITY_EVENT_LOCATION_TYPES = [
13298
13601
  "online",
@@ -13776,6 +14079,40 @@ function registerClientTagsCommands(clients2) {
13776
14079
  var IMPORT_CHUNK_SIZE = 100;
13777
14080
  var SCHEDULER_UNAVAILABLE_STATUS = 503;
13778
14081
  var MAX_BULK_FILTERED_PRODUCTS = 10;
14082
+ function parseGrantPayment(options) {
14083
+ const { paid, currency, paidAt, paymentMethod, paymentNote } = options;
14084
+ if (paid === void 0) {
14085
+ if (currency || paidAt || paymentMethod || paymentNote) {
14086
+ throw new Error("--currency, --paid-at, --payment-method and --payment-note need --paid.");
14087
+ }
14088
+ return void 0;
14089
+ }
14090
+ if (!/^\d+([.,]\d{1,2})?$/.test(paid)) {
14091
+ throw new Error(`Invalid --paid amount: ${paid} (expected e.g. 99 or 99.50)`);
14092
+ }
14093
+ const amountCents = Math.round(Number(paid.replace(",", ".")) * 100);
14094
+ if (amountCents <= 0) {
14095
+ throw new Error("--paid must be above 0.");
14096
+ }
14097
+ if (!currency) {
14098
+ throw new Error("--paid needs --currency (pln, eur, usd or gbp).");
14099
+ }
14100
+ if (paidAt !== void 0 && Number.isNaN(Date.parse(paidAt))) {
14101
+ throw new Error(`Invalid --paid-at date: ${paidAt} (expected ISO, e.g. 2026-10-14)`);
14102
+ }
14103
+ if (paymentMethod !== void 0 && !GRANT_PAYMENT_METHODS.includes(paymentMethod)) {
14104
+ throw new Error(
14105
+ `Invalid --payment-method: ${paymentMethod} (one of ${GRANT_PAYMENT_METHODS.join(", ")})`
14106
+ );
14107
+ }
14108
+ return {
14109
+ amountCents,
14110
+ currency: currency.toLowerCase(),
14111
+ ...paidAt !== void 0 ? { paidAt: new Date(paidAt).toISOString() } : {},
14112
+ ...paymentMethod !== void 0 ? { method: paymentMethod } : {},
14113
+ ...paymentNote !== void 0 ? { note: paymentNote } : {}
14114
+ };
14115
+ }
13779
14116
  function requireConfirm4(confirm, what) {
13780
14117
  if (!confirm) {
13781
14118
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
@@ -14129,6 +14466,33 @@ Next page: --cursor ${page.nextCursor}
14129
14466
  process.stdout.write(` Previous emails: ${detail.previousEmails.join(", ")}
14130
14467
  `);
14131
14468
  }
14469
+ const customFields = Object.entries(detail.customFields ?? {});
14470
+ if (customFields.length > 0) {
14471
+ process.stdout.write(" Custom fields:\n");
14472
+ for (const [key, value] of customFields) {
14473
+ process.stdout.write(` ${key}: ${String(value)}
14474
+ `);
14475
+ }
14476
+ }
14477
+ });
14478
+ });
14479
+ clients2.command("fields").description(
14480
+ "List the custom contact fields (key, name, type, options); set values with `clients update --field key=value`"
14481
+ ).option("--include-archived", "also list archived fields (their values are read-only)").action(async (options, command) => {
14482
+ const flags = getGlobalFlags(command);
14483
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
14484
+ const result = await client.listClientCustomFields(options.includeArchived === true);
14485
+ emit(flags, result, () => {
14486
+ printTable(
14487
+ result.fields.map((field) => ({
14488
+ key: field.key,
14489
+ name: field.name,
14490
+ type: field.type,
14491
+ options: field.options.filter((option) => !option.archived).map((option) => `${option.key}=${option.label}`).join(", "),
14492
+ archived: field.archived ? "yes" : ""
14493
+ })),
14494
+ ["key", "name", "type", "options", "archived"]
14495
+ );
14132
14496
  });
14133
14497
  });
14134
14498
  registerClientTagsCommands(clients2);
@@ -14242,12 +14606,27 @@ Next page: --cursor ${page.nextCursor}
14242
14606
  `);
14243
14607
  });
14244
14608
  });
14245
- clients2.command("update").description("Update a client's profile (name, phone, language)").argument("<id>", "workspace client id (wcid)").option("--first-name <name>", "new first name").option("--last-name <name>", "new last name").option("--phone <number>", "new phone number").option("--language <code>", "new preferred language code (e.g. en, pl)").action(
14609
+ clients2.command("update").description("Update a client's profile (name, phone, language) and custom field values").argument("<id>", "workspace client id (wcid)").option("--first-name <name>", "new first name").option("--last-name <name>", "new last name").option("--phone <number>", "new phone number").option("--language <code>", "new preferred language code (e.g. en, pl)").option(
14610
+ "--field <key=value>",
14611
+ "set a custom field value (repeatable; keys from `clients fields`, e.g. --field plan=premium)",
14612
+ collectRepeatable2,
14613
+ []
14614
+ ).option("--clear-field <key>", "clear a custom field value (repeatable)", collectRepeatable2, []).action(
14246
14615
  async (id, options, command) => {
14247
14616
  const flags = getGlobalFlags(command);
14248
- if (options.firstName === void 0 && options.lastName === void 0 && options.phone === void 0 && options.language === void 0) {
14617
+ const customFields = {};
14618
+ for (const entry of options.field) {
14619
+ const separator = entry.indexOf("=");
14620
+ if (separator <= 0) {
14621
+ throw new Error(`Invalid --field "${entry}": use key=value.`);
14622
+ }
14623
+ customFields[entry.slice(0, separator).trim()] = entry.slice(separator + 1);
14624
+ }
14625
+ for (const key of options.clearField) customFields[key.trim()] = null;
14626
+ const hasCustomFields = Object.keys(customFields).length > 0;
14627
+ if (options.firstName === void 0 && options.lastName === void 0 && options.phone === void 0 && options.language === void 0 && !hasCustomFields) {
14249
14628
  throw new Error(
14250
- "Nothing to update \u2014 pass at least one of --first-name, --last-name, --phone, --language."
14629
+ "Nothing to update \u2014 pass at least one of --first-name, --last-name, --phone, --language, --field, --clear-field."
14251
14630
  );
14252
14631
  }
14253
14632
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -14255,7 +14634,8 @@ Next page: --cursor ${page.nextCursor}
14255
14634
  firstName: options.firstName,
14256
14635
  lastName: options.lastName,
14257
14636
  phoneNumber: options.phone,
14258
- language: options.language
14637
+ language: options.language,
14638
+ ...hasCustomFields ? { customFields } : {}
14259
14639
  });
14260
14640
  emit(flags, result, () => {
14261
14641
  process.stdout.write(`Updated client ${result.clientId}.
@@ -14263,7 +14643,13 @@ Next page: --cursor ${page.nextCursor}
14263
14643
  });
14264
14644
  }
14265
14645
  );
14266
- clients2.command("grant").description("Grant a client access to a product without a purchase").argument("<productId>", "product id").requiredOption("--email <email>", "client email").option("--first-name <name>", "client first name").option("--last-name <name>", "client last name").option("--price <priceId>", "enroll under an existing price without billing").option("--plan <planId>", "plan to enroll in (multi-plan products)").option("--until <date>", "ISO date the access ends (free grants only); omit for lifetime").option("--welcome-email", "send the welcome/sign-in email (default: grant silently)").action(
14646
+ clients2.command("grant").description("Grant a client access to a product without a purchase").argument("<productId>", "product id").requiredOption("--email <email>", "client email").option("--first-name <name>", "client first name").option("--last-name <name>", "client last name").option("--price <priceId>", "enroll under an existing price without billing").option("--plan <planId>", "plan to enroll in (multi-plan products)").option("--until <date>", "ISO date the access ends (free grants only); omit for lifetime").option("--welcome-email", "send the welcome/sign-in email (default: grant silently)").option(
14647
+ "--run-purchase-automations",
14648
+ "also run the product's purchase automations, marked as a grant (default: off)"
14649
+ ).option(
14650
+ "--paid <amount>",
14651
+ "the client already paid outside Zanfia (e.g. 99.50): records one paid order"
14652
+ ).option("--currency <code>", "currency of --paid: pln, eur, usd or gbp").option("--paid-at <date>", "ISO date the client paid (default: now; not in the future)").option("--payment-method <method>", `how they paid: ${GRANT_PAYMENT_METHODS.join(" | ")}`).option("--payment-note <text>", "note on the payment, e.g. the proforma number (max 300)").action(
14267
14653
  async (productId, options, command) => {
14268
14654
  const flags = getGlobalFlags(command);
14269
14655
  if (options.until !== void 0 && Number.isNaN(Date.parse(options.until))) {
@@ -14272,6 +14658,7 @@ Next page: --cursor ${page.nextCursor}
14272
14658
  if (options.until !== void 0 && options.price !== void 0) {
14273
14659
  throw new Error("--until applies to free grants only \u2014 omit --price.");
14274
14660
  }
14661
+ const payment = parseGrantPayment(options);
14275
14662
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
14276
14663
  const result = await client.grantClientAccess(productId, {
14277
14664
  email: options.email,
@@ -14280,13 +14667,14 @@ Next page: --cursor ${page.nextCursor}
14280
14667
  priceId: options.price,
14281
14668
  planId: options.plan,
14282
14669
  accessUntil: options.until,
14283
- sendWelcomeEmail: options.welcomeEmail
14670
+ sendWelcomeEmail: options.welcomeEmail,
14671
+ runPurchaseAutomations: options.runPurchaseAutomations,
14672
+ ...payment ? { payment } : {}
14284
14673
  });
14285
14674
  emit(flags, result, () => {
14286
14675
  const kind = result.isFree ? "free" : "paid";
14287
14676
  process.stdout.write(
14288
- `Granted ${options.email} ${kind} access to product ${productId}.
14289
- `
14677
+ `Granted ${options.email} ${kind} access to product ${productId}` + (payment ? " and recorded the payment as a paid order" : "") + ".\n"
14290
14678
  );
14291
14679
  });
14292
14680
  }
@@ -18615,7 +19003,7 @@ function printOverview(overview) {
18615
19003
  const p = overview.payments;
18616
19004
  const on = (value) => value ? "on" : "off";
18617
19005
  process.stdout.write(
18618
- `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"}
19006
+ `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)} tubapay=${on(p.tubapay)} manual=${p.manual ? `on (${p.manual.name})` : "off"}
18619
19007
  `
18620
19008
  );
18621
19009
  }
@@ -18890,7 +19278,7 @@ function readSecret2(label, value, fromStdin, fromEnv) {
18890
19278
  function printPayments(payments) {
18891
19279
  const on = (value) => value ? "on" : "off";
18892
19280
  process.stdout.write(
18893
- `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"}
19281
+ `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)} tubapay=${on(payments.tubapay)} manual=${payments.manual ? "on" : "off"}
18894
19282
  `
18895
19283
  );
18896
19284
  }
@@ -18969,6 +19357,11 @@ function buildGatewayInput(gateway, o) {
18969
19357
  case "dodo":
18970
19358
  input.apiKey = secret(o.apiKey, "--api-key");
18971
19359
  break;
19360
+ case "tubapay":
19361
+ input.clientId = need(o.clientId, "--client-id");
19362
+ input.clientSecret = secret(o.clientSecret, "--client-secret");
19363
+ input.callbackSecret = secret(o.callbackSecret, "--callback-secret");
19364
+ break;
18972
19365
  }
18973
19366
  return input;
18974
19367
  }
@@ -19009,11 +19402,11 @@ function registerIntegrationProviderCommands(integrations) {
19009
19402
  }
19010
19403
  );
19011
19404
  const payments = integrations.command("payments").description(
19012
- "Credential payment gateways: payu, tpay, przelewy24 (p24), paynow, paypal, dodo. Stripe connects in the dashboard (OAuth); the transfer method is `integrations manual-payment`."
19405
+ "Credential payment gateways: payu, tpay, przelewy24 (p24), paynow, paypal, dodo, tubapay. Stripe connects in the dashboard (OAuth); the transfer method is `integrations manual-payment`."
19013
19406
  );
19014
19407
  payments.command("connect <gateway>").description(
19015
19408
  "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."
19016
- ).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) => {
19409
+ ).requiredOption("--name <name>", "creator-facing label").option("--pos-id <id>", "payu / tpay / przelewy24").option("--client-id <id>", "payu / tpay / paypal; tubapay: the Partner ID").option("--client-secret <secret>", "payu / tpay / paypal / tubapay").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("--callback-secret <key>", "tubapay: the key TubaPay signs its notifications with").option("--secrets-env", "treat every secret flag value as an environment variable name").action(async (gatewayArg, options, command) => {
19017
19410
  const flags = getGlobalFlags(command);
19018
19411
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19019
19412
  const status = await client.connectPaymentGateway(
@@ -19591,15 +19984,28 @@ join again to refresh it.
19591
19984
  // src/commands/liveRooms/index.ts
19592
19985
  var minutes = (seconds) => `${Math.round(seconds / 60)} min`;
19593
19986
  var percent = (part, whole) => whole > 0 ? `${Math.round(part / whole * 100)}%` : "-";
19987
+ var countOrDash = (value) => value === null || value === void 0 ? "-" : String(value);
19594
19988
  var WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
19989
+ function describeExclusions(schedule) {
19990
+ const parts = [];
19991
+ if (schedule.kind !== "daily" && schedule.nightWindow) {
19992
+ parts.push(`none ${schedule.nightWindow.from}-${schedule.nightWindow.to}`);
19993
+ }
19994
+ if (schedule.blackoutDates && schedule.blackoutDates.length > 0) {
19995
+ const days = schedule.blackoutDates.map((range) => range.from === range.to ? range.from : `${range.from}..${range.to}`).join(", ");
19996
+ parts.push(`none on ${days}`);
19997
+ }
19998
+ return parts.length > 0 ? `; ${parts.join("; ")}` : "";
19999
+ }
19595
20000
  function describeSchedule(room) {
19596
20001
  if (!room.schedule) return "fixed";
19597
20002
  const until = room.schedule.until ? ` until ${room.schedule.until}` : "";
20003
+ const exclusions = describeExclusions(room.schedule);
19598
20004
  if (room.schedule.kind === "daily") {
19599
20005
  const days = room.schedule.weekdays ? room.schedule.weekdays.map((day) => WEEKDAYS[day - 1] ?? String(day)).join(", ") : "daily";
19600
- return `${days} at ${room.schedule.times.join(", ")} (${room.timezone})${until}`;
20006
+ return `${days} at ${room.schedule.times.join(", ")} (${room.timezone})${until}${exclusions}`;
19601
20007
  }
19602
- return `every ${room.schedule.everyMinutes} min${until}`;
20008
+ return `every ${room.schedule.everyMinutes} min${until}${exclusions}`;
19603
20009
  }
19604
20010
  function registerLiveRoomsCommands(program2) {
19605
20011
  const liveRooms = program2.command("live-rooms").description(
@@ -19632,6 +20038,7 @@ function registerLiveRoomsCommands(program2) {
19632
20038
  printTable(
19633
20039
  result.sessions.map((session) => ({
19634
20040
  session: session.sessionStartAt,
20041
+ registered: countOrDash(session.registered),
19635
20042
  attendees: session.attendees,
19636
20043
  identified: session.identified,
19637
20044
  avgWatched: `${minutes(session.averageWatchedSeconds)} (${percent(
@@ -19640,10 +20047,29 @@ function registerLiveRoomsCommands(program2) {
19640
20047
  )})`,
19641
20048
  toEnd: `${session.watchedToEnd} (${percent(session.watchedToEnd, session.attendees)})`,
19642
20049
  sawOffer: session.sawOffer,
19643
- clickedOffer: session.clickedOffer
20050
+ clickedOffer: session.clickedOffer,
20051
+ bought: countOrDash(session.purchased)
19644
20052
  })),
19645
- ["session", "attendees", "identified", "avgWatched", "toEnd", "sawOffer", "clickedOffer"]
20053
+ [
20054
+ "session",
20055
+ "registered",
20056
+ "attendees",
20057
+ "identified",
20058
+ "avgWatched",
20059
+ "toEnd",
20060
+ "sawOffer",
20061
+ "clickedOffer",
20062
+ "bought"
20063
+ ]
19646
20064
  );
20065
+ const totals = result.totals;
20066
+ if (totals) {
20067
+ process.stdout.write(
20068
+ `
20069
+ Room total (${totals.sessions} started sessions): registered ${countOrDash(totals.registered)} \u2192 attended ${totals.attendees} \u2192 saw the offer ${totals.sawOffer} \u2192 clicked ${totals.clickedOffer} \u2192 bought ${countOrDash(totals.purchased)}${totals.capped ? " (at least: a read hit its cap)" : ""}
20070
+ `
20071
+ );
20072
+ }
19647
20073
  if (options.session !== void 0) {
19648
20074
  process.stdout.write(`
19649
20075
  Viewers of ${options.session}:
@@ -19905,7 +20331,7 @@ var NoopUrlStorage = /* @__PURE__ */ (function() {
19905
20331
  }]);
19906
20332
  })();
19907
20333
 
19908
- // ../../node_modules/tus-js-client/node_modules/js-base64/base64.mjs
20334
+ // ../../node_modules/js-base64/base64.mjs
19909
20335
  var version = "3.7.8";
19910
20336
  var VERSION = version;
19911
20337
  var _hasBuffer = typeof Buffer === "function";
@@ -25028,6 +25454,7 @@ var PAGE_INTENTS = [
25028
25454
  var SOURCE_SITE_MODES = ["rebuild", "inspired"];
25029
25455
  var EVENT_MODES2 = ["live", "on_demand"];
25030
25456
  var OPT_IN_MODES2 = ["single", "double"];
25457
+ var TRACKING_MODES = ["inherit", "selected", "none"];
25031
25458
  var SCHEDULER_UNAVAILABLE_STATUS3 = 503;
25032
25459
  function parseChoice5(value, allowed, flag) {
25033
25460
  const match = allowed.find((candidate) => candidate === value);
@@ -25164,6 +25591,11 @@ function registerPagesCommands(program2) {
25164
25591
  `);
25165
25592
  process.stdout.write(` Cookie banner: ${page.hideCookieBanner ? "hidden" : "shown"}
25166
25593
  `);
25594
+ const trackingIds = page.tracking?.integrationIds ?? [];
25595
+ process.stdout.write(
25596
+ ` Tracking: ${page.tracking?.mode ?? "inherit"}${trackingIds.length > 0 ? ` (${trackingIds.join(", ")})` : ""}
25597
+ `
25598
+ );
25167
25599
  });
25168
25600
  });
25169
25601
  pages.command("create").description("Create a hosted page from an HTML file or inline HTML").requiredOption("--slug <slug>", "public URL slug (served at /page/<slug>)").requiredOption("--title <title>", "page title").option("--file <path>", "path to an .html file to host").option("--html <html>", "inline HTML body (alternative to --file)").option("--publish", "publish immediately instead of saving as draft").action(async (options, command) => {
@@ -25185,17 +25617,33 @@ function registerPagesCommands(program2) {
25185
25617
  `);
25186
25618
  });
25187
25619
  });
25188
- pages.command("update").description("Update a page (slug, title, HTML body, or cookie-banner visibility)").argument("<id>", "page id").option("--slug <slug>", "new slug").option("--title <title>", "new title").option("--file <path>", "path to a new .html file").option("--html <html>", "new inline HTML body").option("--cookie-banner", "show the workspace cookie banner on this page again").option(
25620
+ pages.command("update").description(
25621
+ "Update a page (slug, title, HTML body, cookie-banner visibility, or its tracking tools)"
25622
+ ).argument("<id>", "page id").option("--slug <slug>", "new slug").option("--title <title>", "new title").option("--file <path>", "path to a new .html file").option("--html <html>", "new inline HTML body").option("--cookie-banner", "show the workspace cookie banner on this page again").option(
25189
25623
  "--no-cookie-banner",
25190
25624
  "hide the workspace cookie banner on this page (pixels still wait for consent given elsewhere)"
25625
+ ).option(
25626
+ "--tracking <mode>",
25627
+ "tracking tools on this page: inherit (workspace defaults), selected (--tracking-integrations) or none"
25628
+ ).option(
25629
+ "--tracking-integrations <ids>",
25630
+ "comma-separated tracking integration ids for --tracking selected (one per platform)"
25191
25631
  ).action(async (id, options, command) => {
25192
25632
  const flags = getGlobalFlags(command);
25193
25633
  const html = await resolveHtml2(options);
25634
+ const trackingIds = options.trackingIntegrations?.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
25635
+ const trackingMode = options.tracking !== void 0 ? parseChoice5(options.tracking, TRACKING_MODES, "--tracking") : trackingIds !== void 0 ? "selected" : void 0;
25194
25636
  const input = {
25195
25637
  slug: options.slug,
25196
25638
  title: options.title,
25197
25639
  html,
25198
- ...options.cookieBanner !== void 0 ? { hideCookieBanner: !options.cookieBanner } : {}
25640
+ ...options.cookieBanner !== void 0 ? { hideCookieBanner: !options.cookieBanner } : {},
25641
+ ...trackingMode !== void 0 ? {
25642
+ tracking: {
25643
+ mode: trackingMode,
25644
+ ...trackingIds !== void 0 ? { integrationIds: trackingIds } : {}
25645
+ }
25646
+ } : {}
25199
25647
  };
25200
25648
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25201
25649
  const result = await client.updatePage(id, input);
@@ -25599,6 +26047,10 @@ function registerProductsCommands(program2) {
25599
26047
  `);
25600
26048
  if (product.storefrontVisibility) {
25601
26049
  process.stdout.write(` Visibility: ${product.storefrontVisibility}
26050
+ `);
26051
+ }
26052
+ if (product.language !== void 0) {
26053
+ process.stdout.write(` Language: ${product.language ?? "none (platform language)"}
25602
26054
  `);
25603
26055
  }
25604
26056
  process.stdout.write(` Clients: ${product.clientCount}
@@ -25618,6 +26070,10 @@ function registerProductsCommands(program2) {
25618
26070
  "--period <period>",
25619
26071
  "billing period for --amount: one_time, monthly or yearly (default one_time)"
25620
26072
  ).option("--description-file <path>", "read the product description from a file").option(
26073
+ "--language <pl|en>",
26074
+ "the product's language (buyer-facing pages and e-mails; omitted = the platform's language). Also picks the --amount currency when --currency is omitted (pl \u2192 pln, else usd)",
26075
+ parseProductLanguage
26076
+ ).option(
25621
26077
  "--cover <pathOrUrl>",
25622
26078
  "cover image: a local file (uploaded to the media library first) or an https URL"
25623
26079
  ).option(
@@ -25638,7 +26094,8 @@ function registerProductsCommands(program2) {
25638
26094
  name: options.name,
25639
26095
  productType: options.type,
25640
26096
  description,
25641
- offer
26097
+ offer,
26098
+ language: options.language
25642
26099
  });
25643
26100
  const outcome = {};
25644
26101
  try {
@@ -25687,21 +26144,28 @@ function registerProductsCommands(program2) {
25687
26144
  });
25688
26145
  }
25689
26146
  );
25690
- products2.command("update").description("Update a product's name, description and/or storefront visibility").argument("<id>", "product id").option("--name <name>", "new product name").option("--description <description>", "new product description").option(
26147
+ products2.command("update").description("Update a product's name, description, storefront visibility and/or language").argument("<id>", "product id").option("--name <name>", "new product name").option("--description <description>", "new product description").option(
25691
26148
  "--visibility <public|unlisted|private>",
25692
26149
  "where buyers find it once published: public (store + product page), unlisted (product page by link only), private (checkout links only)",
25693
26150
  parseStorefrontVisibility
26151
+ ).option(
26152
+ "--language <pl|en|none>",
26153
+ "the product's language (buyer-facing pages and e-mails); none clears it (the platform's language applies)",
26154
+ parseProductLanguageOrNone
25694
26155
  ).action(
25695
26156
  async (id, options, command) => {
25696
- if (options.name === void 0 && options.description === void 0 && options.visibility === void 0) {
25697
- throw new Error("Nothing to update: pass --name, --description and/or --visibility.");
26157
+ if (options.name === void 0 && options.description === void 0 && options.visibility === void 0 && options.language === void 0) {
26158
+ throw new Error(
26159
+ "Nothing to update: pass --name, --description, --visibility and/or --language."
26160
+ );
25698
26161
  }
25699
26162
  const flags = getGlobalFlags(command);
25700
26163
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25701
26164
  const result = await client.updateProduct(id, {
25702
26165
  name: options.name,
25703
26166
  description: options.description,
25704
- storefrontVisibility: options.visibility
26167
+ storefrontVisibility: options.visibility,
26168
+ language: options.language === PRODUCT_LANGUAGE_NONE ? null : options.language
25705
26169
  });
25706
26170
  emit(flags, result, () => {
25707
26171
  process.stdout.write(`Updated product ${result.productId}
@@ -26711,7 +27175,7 @@ function buildUpdateInput2(options) {
26711
27175
  }
26712
27176
  function registerReferralsCommands(program2) {
26713
27177
  const referrals = program2.command("referrals").description(
26714
- "Referral / affiliate programs \u2014 create reward ladders over your products, enroll clients, approve or reject earned rewards, and retry failed reward deliveries"
27178
+ 'Referral programs: "Nagrody za polecenia" (kind referral, reward ladders for your customers) and "Program partnerski" (kind affiliate, partner commission), both from the Growth plan. Create programs over your products, enroll clients, approve or reject earned rewards, and retry failed reward deliveries'
26715
27179
  );
26716
27180
  referrals.command("list").description("List referral programs (kind, status, products, headline stats)").action(async (_, command) => {
26717
27181
  const flags = getGlobalFlags(command);
@@ -26810,7 +27274,7 @@ Failed deliveries (${result.failedDeliveries.length})
26810
27274
  });
26811
27275
  });
26812
27276
  referrals.command("create").description(
26813
- "Create a program over one or more products. Deep config (milestones, commission, attribution) goes in --json; flat flags win over --json keys."
27277
+ "Create a program over one or more products (both kinds need the Growth plan). Deep config (milestones, commission, attribution) goes in --json; flat flags win over --json keys."
26814
27278
  ).requiredOption("--kind <kind>", `program kind (${PROGRAM_KINDS.join(" | ")})`).requiredOption("--name <name>", "program name").requiredOption("--product <ids>", "comma-separated product ids whose purchases count").option("--enrollment <mode>", `how participants join (${ENROLLMENT_MODES.join(" | ")})`).option("--status <status>", `initial status (${WRITABLE_STATUSES.join(" | ")})`).option(
26815
27279
  "--json <fileOrJson>",
26816
27280
  "inline JSON object or .json path with `milestones`, `commission` and/or `attribution` (write it as --json=<value> \u2014 a bare --json is the global JSON-output flag)"
@@ -26825,7 +27289,7 @@ Failed deliveries (${result.failedDeliveries.length})
26825
27289
  });
26826
27290
  });
26827
27291
  referrals.command("update").description(
26828
- "Update a program. --product and --json `milestones` are FULL replaces (read the program first); flat flags win over --json keys. Use `referrals archive` to archive."
27292
+ "Update a program. --product and --json `milestones` are FULL replaces (read the program first); flat flags win over --json keys. Activating (--status active) needs the Growth plan. Use `referrals archive` to archive."
26829
27293
  ).argument("<programId>", "program id").option("--name <name>", "program name").option("--status <status>", `status (${WRITABLE_STATUSES.join(" | ")})`).option("--product <ids>", "comma-separated product ids (full replace)").option("--enrollment <mode>", `how participants join (${ENROLLMENT_MODES.join(" | ")})`).option(
26830
27294
  "--json <fileOrJson>",
26831
27295
  "inline JSON object or .json path with `milestones`, `commission` and/or `attribution` (write it as --json=<value> \u2014 a bare --json is the global JSON-output flag)"
@@ -26853,7 +27317,7 @@ Failed deliveries (${result.failedDeliveries.length})
26853
27317
  });
26854
27318
  });
26855
27319
  referrals.command("enroll").description(
26856
- "Enroll one client as a participant (idempotent \u2014 re-enrolling reports `alreadyEnrolled`). Prints the ref code the client shares."
27320
+ "Enroll one client as a participant (idempotent \u2014 re-enrolling reports `alreadyEnrolled`). Prints the ref code the client shares. Needs the Growth plan."
26857
27321
  ).argument("<programId>", "program id").requiredOption("--client <clientId>", "workspace client id (wcid)").requiredOption("--email <email>", "the client's email").option("--first-name <name>", "first name (optional)").option("--last-name <name>", "last name (optional)").action(async (programId, options, command) => {
26858
27322
  const flags = getGlobalFlags(command);
26859
27323
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -26872,7 +27336,7 @@ Failed deliveries (${result.failedDeliveries.length})
26872
27336
  });
26873
27337
  });
26874
27338
  referrals.command("enroll-all").description(
26875
- "Enroll EVERY client with access to one of the program products. Requires --confirm. Reports enrolled / alreadyEnrolled / skippedWithoutAccount counts."
27339
+ 'Enroll EVERY client with access to one of the program products ("Nagrody za polecenia" only: a "Program partnerski" is refused, partners join by invite link). Needs the Growth plan. Requires --confirm. Reports enrolled / alreadyEnrolled / skippedWithoutAccount counts.'
26876
27340
  ).argument("<programId>", "program id").option("--confirm", "required \u2014 acknowledge the bulk enrollment").action(async (programId, options, command) => {
26877
27341
  const flags = getGlobalFlags(command);
26878
27342
  requireConfirm13(options.confirm, `enroll all clients in program ${programId}`);
@@ -28365,7 +28829,8 @@ var CONVERSION_EVENT_STATUSES = [
28365
28829
  "sent",
28366
28830
  "delivered",
28367
28831
  "failed",
28368
- "consent_blocked"
28832
+ "consent_blocked",
28833
+ "test_skipped"
28369
28834
  ];
28370
28835
  var CONVERSION_TRACKING_CLIENTS = [
28371
28836
  "metaConversionApi",
@@ -28468,12 +28933,12 @@ function registerStatsCommands(program2) {
28468
28933
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
28469
28934
  const report = await client.statsSales(range);
28470
28935
  emit(flags, report, () => {
28471
- const c = report.primaryCurrency;
28936
+ const c = report.currency;
28472
28937
  process.stdout.write(
28473
28938
  `Sales (${report.period.from} \u2192 ${report.period.to}), currency ${c}
28474
- Net revenue: ${money(report.primaryNetRevenueCents)} ${c}
28939
+ Net revenue: ${money(report.totalRevenueCents)} ${c}
28475
28940
  Transactions: ${report.transactions} Refunds: ${report.refunds}
28476
- AOV: ${money(report.aovCents)} ${c} New subscribers: ${report.totalNewSubscribers}
28941
+ AOV: ${money(report.averageOrderValueCents)} ${c} New subscribers: ${report.newSubscribers}
28477
28942
 
28478
28943
  `
28479
28944
  );
@@ -28483,7 +28948,7 @@ function registerStatsCommands(program2) {
28483
28948
  report.perProduct.map((r) => ({
28484
28949
  id: r.id,
28485
28950
  name: r.name,
28486
- revenue: money(r.primaryRevenueCents),
28951
+ revenue: money(r.revenueCents),
28487
28952
  transactions: r.transactions,
28488
28953
  refunds: r.refunds
28489
28954
  })),
@@ -28497,11 +28962,11 @@ function registerStatsCommands(program2) {
28497
28962
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
28498
28963
  const report = await client.statsRecurring(options.currency);
28499
28964
  emit(flags, report, () => {
28500
- const c = report.primaryCurrency;
28965
+ const c = report.currency;
28501
28966
  process.stdout.write(
28502
28967
  `Recurring revenue (currency ${c})
28503
- MRR: ${money(report.primaryMrrCents)} ${c} ARR: ${money(report.primaryArrCents)} ${c}
28504
- Active subscriptions: ${report.activeRecurringSubscriptions} Ending soon: ${report.endingSoonCount}
28968
+ MRR: ${money(report.mrrCents)} ${c} ARR: ${money(report.arrCents)} ${c}
28969
+ Active subscriptions: ${report.activeSubscriptions} Ending soon: ${report.endingSoonCount}
28505
28970
 
28506
28971
  `
28507
28972
  );
@@ -28510,8 +28975,8 @@ function registerStatsCommands(program2) {
28510
28975
  report.perProduct.map((r) => ({
28511
28976
  id: r.productId,
28512
28977
  name: r.productName,
28513
- mrr: money(r.primaryMrrCents),
28514
- subs: r.activeRecurringSubscriptions
28978
+ mrr: money(r.mrrCents),
28979
+ subs: r.activeSubscriptions
28515
28980
  })),
28516
28981
  ["id", "name", "mrr", "subs"]
28517
28982
  );
@@ -28526,13 +28991,13 @@ function registerStatsCommands(program2) {
28526
28991
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
28527
28992
  const report = await client.statsMrrHistory(range);
28528
28993
  emit(flags, report, () => {
28529
- process.stdout.write(`MRR history (currency ${report.primaryCurrency})
28994
+ process.stdout.write(`MRR history (currency ${report.currency})
28530
28995
  `);
28531
28996
  printTable(
28532
28997
  report.points.map((p) => ({
28533
28998
  date: p.date,
28534
28999
  mrr: money(p.mrrCents),
28535
- subs: p.activeRecurringSubscriptions,
29000
+ subs: p.activeSubscriptions,
28536
29001
  new: money(p.newMrrCents),
28537
29002
  churned: money(p.churnedMrrCents),
28538
29003
  churnRate: `${(p.churnRate * 100).toFixed(1)}%`
@@ -28612,11 +29077,23 @@ function registerStatsCommands(program2) {
28612
29077
  checkout: s.checkoutId,
28613
29078
  stage: s.furthestStage,
28614
29079
  status: s.status,
28615
- visitor: s.visitorLabel,
28616
- source: s.source ?? "",
29080
+ converted: s.converted ? "yes" : "",
29081
+ source: s.source.utmSource ?? s.source.referrer ?? "",
29082
+ country: s.country ?? "",
29083
+ declined: s.paymentFailure?.reason ?? "",
28617
29084
  startedAt: s.startedAt
28618
29085
  })),
28619
- ["id", "checkout", "stage", "status", "visitor", "source", "startedAt"]
29086
+ [
29087
+ "id",
29088
+ "checkout",
29089
+ "stage",
29090
+ "status",
29091
+ "converted",
29092
+ "source",
29093
+ "country",
29094
+ "declined",
29095
+ "startedAt"
29096
+ ]
28620
29097
  );
28621
29098
  });
28622
29099
  });
@@ -28625,7 +29102,7 @@ function registerStatsCommands(program2) {
28625
29102
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
28626
29103
  const report = await client.statsProduct(productId, options.currency);
28627
29104
  emit(flags, report, () => {
28628
- const c = report.primaryCurrency;
29105
+ const c = report.currency;
28629
29106
  process.stdout.write(
28630
29107
  `Product ${report.productId} (${report.window.startDate} \u2192 ${report.window.endDate}), currency ${c}
28631
29108
  Revenue: ${money(report.revenue.totalCents)} ${c} (prev ${money(report.revenue.previousTotalCents)} ${c})
@@ -30286,8 +30763,8 @@ function applyShortHelp(program2) {
30286
30763
 
30287
30764
  // src/program.ts
30288
30765
  function resolveCliVersion() {
30289
- if ("0.6.4".length > 0) {
30290
- return "0.6.4";
30766
+ if ("0.10.0".length > 0) {
30767
+ return "0.10.0";
30291
30768
  }
30292
30769
  try {
30293
30770
  const raw = readFileSync28(new URL("../package.json", import.meta.url), "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zanfia/cli",
3
- "version": "0.6.4",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "Zanfia CLI — thin HTTP client over the Zanfia Public API",
6
6
  "type": "module",