@zanfia/cli 0.9.0 → 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 +417 -30
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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 = [
@@ -10327,18 +10355,19 @@ function registerCertificatesCommands(program2) {
10327
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 certificates of the workspace (course completion and event participation), newest first, optionally filtered to one product (`productId`); `limit` caps the rows (service default applies). 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."
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."
10331
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");
@@ -10699,10 +10728,270 @@ function registerBlogCommands(program2) {
10699
10728
  });
10700
10729
  }
10701
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
+
10702
10989
  // src/commands/_generated/index.ts
10703
10990
  function registerGeneratedCommands(program2) {
10704
10991
  registerCertificatesCommands(program2);
10705
10992
  registerBlogCommands(program2);
10993
+ registerSiteRedirectsCommands(program2);
10994
+ registerPopupsCommands(program2);
10706
10995
  }
10707
10996
 
10708
10997
  // src/commands/ads/index.ts
@@ -13305,7 +13594,8 @@ var CREDENTIAL_PAYMENT_GATEWAYS = [
13305
13594
  "przelewy24",
13306
13595
  "paynow",
13307
13596
  "paypal",
13308
- "dodo"
13597
+ "dodo",
13598
+ "tubapay"
13309
13599
  ];
13310
13600
  var COMMUNITY_EVENT_LOCATION_TYPES = [
13311
13601
  "online",
@@ -14176,6 +14466,33 @@ Next page: --cursor ${page.nextCursor}
14176
14466
  process.stdout.write(` Previous emails: ${detail.previousEmails.join(", ")}
14177
14467
  `);
14178
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
+ );
14179
14496
  });
14180
14497
  });
14181
14498
  registerClientTagsCommands(clients2);
@@ -14289,12 +14606,27 @@ Next page: --cursor ${page.nextCursor}
14289
14606
  `);
14290
14607
  });
14291
14608
  });
14292
- 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(
14293
14615
  async (id, options, command) => {
14294
14616
  const flags = getGlobalFlags(command);
14295
- 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) {
14296
14628
  throw new Error(
14297
- "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."
14298
14630
  );
14299
14631
  }
14300
14632
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -14302,7 +14634,8 @@ Next page: --cursor ${page.nextCursor}
14302
14634
  firstName: options.firstName,
14303
14635
  lastName: options.lastName,
14304
14636
  phoneNumber: options.phone,
14305
- language: options.language
14637
+ language: options.language,
14638
+ ...hasCustomFields ? { customFields } : {}
14306
14639
  });
14307
14640
  emit(flags, result, () => {
14308
14641
  process.stdout.write(`Updated client ${result.clientId}.
@@ -18670,7 +19003,7 @@ function printOverview(overview) {
18670
19003
  const p = overview.payments;
18671
19004
  const on = (value) => value ? "on" : "off";
18672
19005
  process.stdout.write(
18673
- `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"}
18674
19007
  `
18675
19008
  );
18676
19009
  }
@@ -18945,7 +19278,7 @@ function readSecret2(label, value, fromStdin, fromEnv) {
18945
19278
  function printPayments(payments) {
18946
19279
  const on = (value) => value ? "on" : "off";
18947
19280
  process.stdout.write(
18948
- `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"}
18949
19282
  `
18950
19283
  );
18951
19284
  }
@@ -19024,6 +19357,11 @@ function buildGatewayInput(gateway, o) {
19024
19357
  case "dodo":
19025
19358
  input.apiKey = secret(o.apiKey, "--api-key");
19026
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;
19027
19365
  }
19028
19366
  return input;
19029
19367
  }
@@ -19064,11 +19402,11 @@ function registerIntegrationProviderCommands(integrations) {
19064
19402
  }
19065
19403
  );
19066
19404
  const payments = integrations.command("payments").description(
19067
- "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`."
19068
19406
  );
19069
19407
  payments.command("connect <gateway>").description(
19070
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."
19071
- ).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) => {
19072
19410
  const flags = getGlobalFlags(command);
19073
19411
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19074
19412
  const status = await client.connectPaymentGateway(
@@ -19646,15 +19984,28 @@ join again to refresh it.
19646
19984
  // src/commands/liveRooms/index.ts
19647
19985
  var minutes = (seconds) => `${Math.round(seconds / 60)} min`;
19648
19986
  var percent = (part, whole) => whole > 0 ? `${Math.round(part / whole * 100)}%` : "-";
19987
+ var countOrDash = (value) => value === null || value === void 0 ? "-" : String(value);
19649
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
+ }
19650
20000
  function describeSchedule(room) {
19651
20001
  if (!room.schedule) return "fixed";
19652
20002
  const until = room.schedule.until ? ` until ${room.schedule.until}` : "";
20003
+ const exclusions = describeExclusions(room.schedule);
19653
20004
  if (room.schedule.kind === "daily") {
19654
20005
  const days = room.schedule.weekdays ? room.schedule.weekdays.map((day) => WEEKDAYS[day - 1] ?? String(day)).join(", ") : "daily";
19655
- return `${days} at ${room.schedule.times.join(", ")} (${room.timezone})${until}`;
20006
+ return `${days} at ${room.schedule.times.join(", ")} (${room.timezone})${until}${exclusions}`;
19656
20007
  }
19657
- return `every ${room.schedule.everyMinutes} min${until}`;
20008
+ return `every ${room.schedule.everyMinutes} min${until}${exclusions}`;
19658
20009
  }
19659
20010
  function registerLiveRoomsCommands(program2) {
19660
20011
  const liveRooms = program2.command("live-rooms").description(
@@ -19687,6 +20038,7 @@ function registerLiveRoomsCommands(program2) {
19687
20038
  printTable(
19688
20039
  result.sessions.map((session) => ({
19689
20040
  session: session.sessionStartAt,
20041
+ registered: countOrDash(session.registered),
19690
20042
  attendees: session.attendees,
19691
20043
  identified: session.identified,
19692
20044
  avgWatched: `${minutes(session.averageWatchedSeconds)} (${percent(
@@ -19695,10 +20047,29 @@ function registerLiveRoomsCommands(program2) {
19695
20047
  )})`,
19696
20048
  toEnd: `${session.watchedToEnd} (${percent(session.watchedToEnd, session.attendees)})`,
19697
20049
  sawOffer: session.sawOffer,
19698
- clickedOffer: session.clickedOffer
20050
+ clickedOffer: session.clickedOffer,
20051
+ bought: countOrDash(session.purchased)
19699
20052
  })),
19700
- ["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
+ ]
19701
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
+ }
19702
20073
  if (options.session !== void 0) {
19703
20074
  process.stdout.write(`
19704
20075
  Viewers of ${options.session}:
@@ -25676,6 +26047,10 @@ function registerProductsCommands(program2) {
25676
26047
  `);
25677
26048
  if (product.storefrontVisibility) {
25678
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)"}
25679
26054
  `);
25680
26055
  }
25681
26056
  process.stdout.write(` Clients: ${product.clientCount}
@@ -25695,6 +26070,10 @@ function registerProductsCommands(program2) {
25695
26070
  "--period <period>",
25696
26071
  "billing period for --amount: one_time, monthly or yearly (default one_time)"
25697
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(
25698
26077
  "--cover <pathOrUrl>",
25699
26078
  "cover image: a local file (uploaded to the media library first) or an https URL"
25700
26079
  ).option(
@@ -25715,7 +26094,8 @@ function registerProductsCommands(program2) {
25715
26094
  name: options.name,
25716
26095
  productType: options.type,
25717
26096
  description,
25718
- offer
26097
+ offer,
26098
+ language: options.language
25719
26099
  });
25720
26100
  const outcome = {};
25721
26101
  try {
@@ -25764,21 +26144,28 @@ function registerProductsCommands(program2) {
25764
26144
  });
25765
26145
  }
25766
26146
  );
25767
- 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(
25768
26148
  "--visibility <public|unlisted|private>",
25769
26149
  "where buyers find it once published: public (store + product page), unlisted (product page by link only), private (checkout links only)",
25770
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
25771
26155
  ).action(
25772
26156
  async (id, options, command) => {
25773
- if (options.name === void 0 && options.description === void 0 && options.visibility === void 0) {
25774
- 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
+ );
25775
26161
  }
25776
26162
  const flags = getGlobalFlags(command);
25777
26163
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25778
26164
  const result = await client.updateProduct(id, {
25779
26165
  name: options.name,
25780
26166
  description: options.description,
25781
- storefrontVisibility: options.visibility
26167
+ storefrontVisibility: options.visibility,
26168
+ language: options.language === PRODUCT_LANGUAGE_NONE ? null : options.language
25782
26169
  });
25783
26170
  emit(flags, result, () => {
25784
26171
  process.stdout.write(`Updated product ${result.productId}
@@ -26788,7 +27175,7 @@ function buildUpdateInput2(options) {
26788
27175
  }
26789
27176
  function registerReferralsCommands(program2) {
26790
27177
  const referrals = program2.command("referrals").description(
26791
- "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'
26792
27179
  );
26793
27180
  referrals.command("list").description("List referral programs (kind, status, products, headline stats)").action(async (_, command) => {
26794
27181
  const flags = getGlobalFlags(command);
@@ -26887,7 +27274,7 @@ Failed deliveries (${result.failedDeliveries.length})
26887
27274
  });
26888
27275
  });
26889
27276
  referrals.command("create").description(
26890
- "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."
26891
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(
26892
27279
  "--json <fileOrJson>",
26893
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)"
@@ -26902,7 +27289,7 @@ Failed deliveries (${result.failedDeliveries.length})
26902
27289
  });
26903
27290
  });
26904
27291
  referrals.command("update").description(
26905
- "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."
26906
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(
26907
27294
  "--json <fileOrJson>",
26908
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)"
@@ -26930,7 +27317,7 @@ Failed deliveries (${result.failedDeliveries.length})
26930
27317
  });
26931
27318
  });
26932
27319
  referrals.command("enroll").description(
26933
- "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."
26934
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) => {
26935
27322
  const flags = getGlobalFlags(command);
26936
27323
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -26949,7 +27336,7 @@ Failed deliveries (${result.failedDeliveries.length})
26949
27336
  });
26950
27337
  });
26951
27338
  referrals.command("enroll-all").description(
26952
- "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.'
26953
27340
  ).argument("<programId>", "program id").option("--confirm", "required \u2014 acknowledge the bulk enrollment").action(async (programId, options, command) => {
26954
27341
  const flags = getGlobalFlags(command);
26955
27342
  requireConfirm13(options.confirm, `enroll all clients in program ${programId}`);
@@ -30376,8 +30763,8 @@ function applyShortHelp(program2) {
30376
30763
 
30377
30764
  // src/program.ts
30378
30765
  function resolveCliVersion() {
30379
- if ("0.9.0".length > 0) {
30380
- return "0.9.0";
30766
+ if ("0.10.0".length > 0) {
30767
+ return "0.10.0";
30381
30768
  }
30382
30769
  try {
30383
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.9.0",
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",