@zanfia/cli 0.10.0 → 0.21.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 (3) hide show
  1. package/README.md +16 -0
  2. package/dist/index.js +1961 -244
  3. package/package.json +38 -38
package/dist/index.js CHANGED
@@ -1024,7 +1024,7 @@ var require_command = __commonJS({
1024
1024
  writeErr: (str) => process2.stderr.write(str),
1025
1025
  getOutHelpWidth: () => process2.stdout.isTTY ? process2.stdout.columns : void 0,
1026
1026
  getErrHelpWidth: () => process2.stderr.isTTY ? process2.stderr.columns : void 0,
1027
- outputError: (str, write) => write(str)
1027
+ outputError: (str, write2) => write2(str)
1028
1028
  };
1029
1029
  this._hidden = false;
1030
1030
  this._helpOption = void 0;
@@ -2824,13 +2824,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
2824
2824
  _getHelpContext(contextOptions) {
2825
2825
  contextOptions = contextOptions || {};
2826
2826
  const context = { error: !!contextOptions.error };
2827
- let write;
2827
+ let write2;
2828
2828
  if (context.error) {
2829
- write = (arg) => this._outputConfiguration.writeErr(arg);
2829
+ write2 = (arg) => this._outputConfiguration.writeErr(arg);
2830
2830
  } else {
2831
- write = (arg) => this._outputConfiguration.writeOut(arg);
2831
+ write2 = (arg) => this._outputConfiguration.writeOut(arg);
2832
2832
  }
2833
- context.write = contextOptions.write || write;
2833
+ context.write = contextOptions.write || write2;
2834
2834
  context.command = this;
2835
2835
  return context;
2836
2836
  }
@@ -3221,9 +3221,9 @@ var require_url_parse = __commonJS({
3221
3221
  rest
3222
3222
  };
3223
3223
  }
3224
- function resolve2(relative, base) {
3225
- if (relative === "") return base;
3226
- var path2 = (base || "/").split("/").slice(0, -1).concat(relative.split("/")), i = path2.length, last = path2[i - 1], unshift = false, up = 0;
3224
+ function resolve3(relative2, base) {
3225
+ if (relative2 === "") return base;
3226
+ var path2 = (base || "/").split("/").slice(0, -1).concat(relative2.split("/")), i = path2.length, last = path2[i - 1], unshift = false, up = 0;
3227
3227
  while (i--) {
3228
3228
  if (path2[i] === ".") {
3229
3229
  path2.splice(i, 1);
@@ -3246,7 +3246,7 @@ var require_url_parse = __commonJS({
3246
3246
  if (!(this instanceof Url)) {
3247
3247
  return new Url(address, location, parser);
3248
3248
  }
3249
- var relative, extracted, parse2, instruction, index, key, instructions = rules.slice(), type = typeof location, url = this, i = 0;
3249
+ var relative2, extracted, parse2, instruction, index, key, instructions = rules.slice(), type = typeof location, url = this, i = 0;
3250
3250
  if ("object" !== type && "string" !== type) {
3251
3251
  parser = location;
3252
3252
  location = null;
@@ -3254,8 +3254,8 @@ var require_url_parse = __commonJS({
3254
3254
  if (parser && "function" !== typeof parser) parser = qs.parse;
3255
3255
  location = lolcation(location);
3256
3256
  extracted = extractProtocol(address || "", location);
3257
- relative = !extracted.protocol && !extracted.slashes;
3258
- url.slashes = extracted.slashes || relative && location.slashes;
3257
+ relative2 = !extracted.protocol && !extracted.slashes;
3258
+ url.slashes = extracted.slashes || relative2 && location.slashes;
3259
3259
  url.protocol = extracted.protocol || location.protocol || "";
3260
3260
  address = extracted.rest;
3261
3261
  if (extracted.protocol === "file:" && (extracted.slashesCount !== 2 || windowsDriveLetter.test(address)) || !extracted.slashes && (extracted.protocol || extracted.slashesCount < 2 || !isSpecial(url.protocol))) {
@@ -3286,12 +3286,12 @@ var require_url_parse = __commonJS({
3286
3286
  url[key] = index[1];
3287
3287
  address = address.slice(0, index.index);
3288
3288
  }
3289
- url[key] = url[key] || (relative && instruction[3] ? location[key] || "" : "");
3289
+ url[key] = url[key] || (relative2 && instruction[3] ? location[key] || "" : "");
3290
3290
  if (instruction[4]) url[key] = url[key].toLowerCase();
3291
3291
  }
3292
3292
  if (parser) url.query = parser(url.query);
3293
- if (relative && location.slashes && url.pathname.charAt(0) !== "/" && (url.pathname !== "" || location.pathname !== "")) {
3294
- url.pathname = resolve2(url.pathname, location.pathname);
3293
+ if (relative2 && location.slashes && url.pathname.charAt(0) !== "/" && (url.pathname !== "" || location.pathname !== "")) {
3294
+ url.pathname = resolve3(url.pathname, location.pathname);
3295
3295
  }
3296
3296
  if (url.pathname.charAt(0) !== "/" && isSpecial(url.protocol)) {
3297
3297
  url.pathname = "/" + url.pathname;
@@ -5793,8 +5793,8 @@ var require_graceful_fs = __commonJS({
5793
5793
  fs3.createReadStream = createReadStream3;
5794
5794
  fs3.createWriteStream = createWriteStream;
5795
5795
  var fs$readFile = fs3.readFile;
5796
- fs3.readFile = readFile4;
5797
- function readFile4(path2, options, cb) {
5796
+ fs3.readFile = readFile6;
5797
+ function readFile6(path2, options, cb) {
5798
5798
  if (typeof options === "function")
5799
5799
  cb = options, options = null;
5800
5800
  return go$readFile(path2, options, cb);
@@ -5865,9 +5865,9 @@ var require_graceful_fs = __commonJS({
5865
5865
  }
5866
5866
  }
5867
5867
  var fs$readdir = fs3.readdir;
5868
- fs3.readdir = readdir;
5868
+ fs3.readdir = readdir2;
5869
5869
  var noReaddirOptionVersions = /^v[0-5]\./;
5870
- function readdir(path2, options, cb) {
5870
+ function readdir2(path2, options, cb) {
5871
5871
  if (typeof options === "function")
5872
5872
  cb = options, options = null;
5873
5873
  var go$readdir = noReaddirOptionVersions.test(process.version) ? function go$readdir2(path3, options2, cb2, startTime) {
@@ -6198,11 +6198,11 @@ var require_retry_operation = __commonJS({
6198
6198
  for (var i = 0; i < this._errors.length; i++) {
6199
6199
  var error = this._errors[i];
6200
6200
  var message = error.message;
6201
- var count = (counts[message] || 0) + 1;
6202
- counts[message] = count;
6203
- if (count >= mainErrorCount) {
6201
+ var count2 = (counts[message] || 0) + 1;
6202
+ counts[message] = count2;
6203
+ if (count2 >= mainErrorCount) {
6204
6204
  mainError = error;
6205
- mainErrorCount = count;
6205
+ mainErrorCount = count2;
6206
6206
  }
6207
6207
  }
6208
6208
  return mainError;
@@ -6511,11 +6511,11 @@ var require_mtime_precision = __commonJS({
6511
6511
  function probe(file, fs2, callback) {
6512
6512
  const cachedPrecision = fs2[cacheSymbol];
6513
6513
  if (cachedPrecision) {
6514
- return fs2.stat(file, (err, stat4) => {
6514
+ return fs2.stat(file, (err, stat6) => {
6515
6515
  if (err) {
6516
6516
  return callback(err);
6517
6517
  }
6518
- callback(null, stat4.mtime, cachedPrecision);
6518
+ callback(null, stat6.mtime, cachedPrecision);
6519
6519
  });
6520
6520
  }
6521
6521
  const mtime = new Date(Math.ceil(Date.now() / 1e3) * 1e3 + 5);
@@ -6523,13 +6523,13 @@ var require_mtime_precision = __commonJS({
6523
6523
  if (err) {
6524
6524
  return callback(err);
6525
6525
  }
6526
- fs2.stat(file, (err2, stat4) => {
6526
+ fs2.stat(file, (err2, stat6) => {
6527
6527
  if (err2) {
6528
6528
  return callback(err2);
6529
6529
  }
6530
- const precision = stat4.mtime.getTime() % 1e3 === 0 ? "s" : "ms";
6530
+ const precision = stat6.mtime.getTime() % 1e3 === 0 ? "s" : "ms";
6531
6531
  Object.defineProperty(fs2, cacheSymbol, { value: precision });
6532
- callback(null, stat4.mtime, precision);
6532
+ callback(null, stat6.mtime, precision);
6533
6533
  });
6534
6534
  });
6535
6535
  }
@@ -6583,14 +6583,14 @@ var require_lockfile = __commonJS({
6583
6583
  if (options.stale <= 0) {
6584
6584
  return callback(Object.assign(new Error("Lock file is already being held"), { code: "ELOCKED", file }));
6585
6585
  }
6586
- options.fs.stat(lockfilePath, (err2, stat4) => {
6586
+ options.fs.stat(lockfilePath, (err2, stat6) => {
6587
6587
  if (err2) {
6588
6588
  if (err2.code === "ENOENT") {
6589
6589
  return acquireLock(file, { ...options, stale: 0 }, callback);
6590
6590
  }
6591
6591
  return callback(err2);
6592
6592
  }
6593
- if (!isLockStale(stat4, options)) {
6593
+ if (!isLockStale(stat6, options)) {
6594
6594
  return callback(Object.assign(new Error("Lock file is already being held"), { code: "ELOCKED", file }));
6595
6595
  }
6596
6596
  removeLock(file, options, (err3) => {
@@ -6602,8 +6602,8 @@ var require_lockfile = __commonJS({
6602
6602
  });
6603
6603
  });
6604
6604
  }
6605
- function isLockStale(stat4, options) {
6606
- return stat4.mtime.getTime() < Date.now() - options.stale;
6605
+ function isLockStale(stat6, options) {
6606
+ return stat6.mtime.getTime() < Date.now() - options.stale;
6607
6607
  }
6608
6608
  function removeLock(file, options, callback) {
6609
6609
  options.fs.rmdir(getLockFile(file, options), (err) => {
@@ -6621,7 +6621,7 @@ var require_lockfile = __commonJS({
6621
6621
  lock3.updateDelay = lock3.updateDelay || options.update;
6622
6622
  lock3.updateTimeout = setTimeout(() => {
6623
6623
  lock3.updateTimeout = null;
6624
- options.fs.stat(lock3.lockfilePath, (err, stat4) => {
6624
+ options.fs.stat(lock3.lockfilePath, (err, stat6) => {
6625
6625
  const isOverThreshold = lock3.lastUpdate + options.stale < Date.now();
6626
6626
  if (err) {
6627
6627
  if (err.code === "ENOENT" || isOverThreshold) {
@@ -6630,7 +6630,7 @@ var require_lockfile = __commonJS({
6630
6630
  lock3.updateDelay = 1e3;
6631
6631
  return updateLock(file, options);
6632
6632
  }
6633
- const isMtimeOurs = lock3.mtime.getTime() === stat4.mtime.getTime();
6633
+ const isMtimeOurs = lock3.mtime.getTime() === stat6.mtime.getTime();
6634
6634
  if (!isMtimeOurs) {
6635
6635
  return setLockAsCompromised(
6636
6636
  file,
@@ -6755,11 +6755,11 @@ var require_lockfile = __commonJS({
6755
6755
  if (err) {
6756
6756
  return callback(err);
6757
6757
  }
6758
- options.fs.stat(getLockFile(file2, options), (err2, stat4) => {
6758
+ options.fs.stat(getLockFile(file2, options), (err2, stat6) => {
6759
6759
  if (err2) {
6760
6760
  return err2.code === "ENOENT" ? callback(null, false) : callback(err2);
6761
6761
  }
6762
- return callback(null, !isLockStale(stat4, options));
6762
+ return callback(null, !isLockStale(stat6, options));
6763
6763
  });
6764
6764
  });
6765
6765
  }
@@ -6805,12 +6805,12 @@ var require_adapter = __commonJS({
6805
6805
  return newFs;
6806
6806
  }
6807
6807
  function toPromise(method) {
6808
- return (...args) => new Promise((resolve2, reject) => {
6808
+ return (...args) => new Promise((resolve3, reject) => {
6809
6809
  args.push((err, result) => {
6810
6810
  if (err) {
6811
6811
  reject(err);
6812
6812
  } else {
6813
- resolve2(result);
6813
+ resolve3(result);
6814
6814
  }
6815
6815
  });
6816
6816
  method(...args);
@@ -6909,6 +6909,7 @@ import { homedir } from "os";
6909
6909
  import { dirname, join } from "path";
6910
6910
  var CREDENTIALS_STORE_VERSION = 2;
6911
6911
  var MEMBER_PROFILE_PREFIX = "member:";
6912
+ var AGENT_PROFILE_PREFIX = "agent:";
6912
6913
  var FALLBACK_PROFILE_KEY = "default";
6913
6914
  function configDir() {
6914
6915
  return process.env.ZANFIA_CONFIG_DIR || join(homedir(), ".config", "zanfia");
@@ -6947,6 +6948,9 @@ function profileKeyFor(credentials) {
6947
6948
  const claims = decodeTokenClaims(credentials.apiKey);
6948
6949
  const kind = credentials.kind ?? claims.kind;
6949
6950
  const base = credentials.workspaceId ?? claims.workspaceId ?? FALLBACK_PROFILE_KEY;
6951
+ if (kind === "agent") {
6952
+ return `${AGENT_PROFILE_PREFIX}${claims.agentId ?? FALLBACK_PROFILE_KEY}@${base}`;
6953
+ }
6950
6954
  return kind === "member" ? `${MEMBER_PROFILE_PREFIX}${base}` : base;
6951
6955
  }
6952
6956
  function parseCredentialsStore(raw) {
@@ -7047,6 +7051,18 @@ function resolveApiKey(explicit) {
7047
7051
  const stored = readCredentials();
7048
7052
  return stored?.apiKey ?? null;
7049
7053
  }
7054
+ var MEMBER_WRITE_SCOPE = "communities:write";
7055
+ var FULL_MEMBER_SCOPES = ["communities:read", "communities:write"];
7056
+ function memberKeyScopes(claims) {
7057
+ return claims.scopes ?? [...FULL_MEMBER_SCOPES];
7058
+ }
7059
+ function isReadOnlyMemberKey(claims) {
7060
+ return !memberKeyScopes(claims).includes(MEMBER_WRITE_SCOPE);
7061
+ }
7062
+ function memberScopeLabel(claims) {
7063
+ const scopes = memberKeyScopes(claims).join(", ");
7064
+ return isReadOnlyMemberKey(claims) ? `${scopes} (read-only)` : scopes;
7065
+ }
7050
7066
  function decodeTokenClaims(token) {
7051
7067
  if (!token) return { kind: "workspace" };
7052
7068
  const parts = token.split(".");
@@ -7054,15 +7070,21 @@ function decodeTokenClaims(token) {
7054
7070
  try {
7055
7071
  const p = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
7056
7072
  return {
7057
- kind: p.kind === "member" ? "member" : "workspace",
7073
+ kind: tokenKindOf(p.kind),
7058
7074
  workspaceId: typeof p.wsp === "string" ? p.wsp : void 0,
7075
+ agentId: typeof p.agt === "string" ? p.agt : void 0,
7059
7076
  subject: typeof p.sub === "string" ? p.sub : void 0,
7060
- expiresAt: typeof p.exp === "number" ? new Date(p.exp * 1e3).toISOString() : void 0
7077
+ expiresAt: typeof p.exp === "number" ? new Date(p.exp * 1e3).toISOString() : void 0,
7078
+ scopes: typeof p.scope === "string" ? p.scope.split(" ").filter(Boolean) : void 0
7061
7079
  };
7062
7080
  } catch {
7063
7081
  return { kind: "workspace" };
7064
7082
  }
7065
7083
  }
7084
+ function tokenKindOf(claim) {
7085
+ if (claim === "member" || claim === "agent") return claim;
7086
+ return "workspace";
7087
+ }
7066
7088
  function decodeTokenKind(token) {
7067
7089
  return decodeTokenClaims(token).kind;
7068
7090
  }
@@ -8338,6 +8360,9 @@ function createMockClient() {
8338
8360
  async activatePageAutomations() {
8339
8361
  return { delivery: null, followUps: [] };
8340
8362
  },
8363
+ async startPageFamilyStep() {
8364
+ throw new ApiError(501, "page family steps are not available in mock mode");
8365
+ },
8341
8366
  async listSocialAccounts() {
8342
8367
  return { count: 0, accounts: [] };
8343
8368
  },
@@ -8361,6 +8386,42 @@ function createMockClient() {
8361
8386
  async publishSocialPost(id) {
8362
8387
  return { postId: id, status: "published", results: [] };
8363
8388
  },
8389
+ async updateSocialPost(id) {
8390
+ throw new ApiError(404, `Social post ${id} not found`);
8391
+ },
8392
+ async unscheduleSocialPost(id) {
8393
+ throw new ApiError(404, `Social post ${id} not found`);
8394
+ },
8395
+ async deleteSocialPost(id) {
8396
+ return { postId: id, deleted: true, previousStatus: "draft" };
8397
+ },
8398
+ async getYouTubeChannel() {
8399
+ throw new ApiError(404, "No YouTube account is connected to this workspace (mock mode)");
8400
+ },
8401
+ async getYouTubeChannelAnalytics() {
8402
+ throw new ApiError(404, "No YouTube account is connected to this workspace (mock mode)");
8403
+ },
8404
+ async listYouTubeVideos() {
8405
+ return { videos: [], nextPageToken: null };
8406
+ },
8407
+ async getYouTubeVideo(videoId) {
8408
+ throw new ApiError(404, `Video ${videoId} is not a video of the connected channel (mock)`);
8409
+ },
8410
+ async getYouTubeVideoTranscript(videoId) {
8411
+ throw new ApiError(404, `Video ${videoId} is not a video of the connected channel (mock)`);
8412
+ },
8413
+ async getInstagramInsights() {
8414
+ throw new ApiError(404, "No Instagram account is connected to this workspace (mock mode)");
8415
+ },
8416
+ async listInstagramMedia() {
8417
+ return { items: [], nextCursor: null };
8418
+ },
8419
+ async listThreadsPosts() {
8420
+ return { items: [] };
8421
+ },
8422
+ async getLatestSocialSnapshot() {
8423
+ return { snapshot: null };
8424
+ },
8364
8425
  async uploadMedia(input) {
8365
8426
  return {
8366
8427
  mediaId: "media_mock",
@@ -8629,6 +8690,12 @@ function createMockClient() {
8629
8690
  async reorderDownloadFiles() {
8630
8691
  throw new ApiError(501, "downloads content is not available in mock mode");
8631
8692
  },
8693
+ async getProductFileSet() {
8694
+ throw new ApiError(501, "product files are not available in mock mode");
8695
+ },
8696
+ async changeProductFileSet() {
8697
+ throw new ApiError(501, "product files are not available in mock mode");
8698
+ },
8632
8699
  async listNewsletterIssues() {
8633
8700
  throw new ApiError(501, "newsletter content is not available in mock mode");
8634
8701
  },
@@ -8782,6 +8849,9 @@ function createMockClient() {
8782
8849
  async deleteCommunityEvent() {
8783
8850
  throw new ApiError(501, "community events are not available in mock mode");
8784
8851
  },
8852
+ async listCommunityEventAttendees() {
8853
+ throw new ApiError(501, "community events are not available in mock mode");
8854
+ },
8785
8855
  async getCommunityModerationQueue() {
8786
8856
  return { pending: [], resolved: [] };
8787
8857
  },
@@ -8852,7 +8922,7 @@ function createMockClient() {
8852
8922
  return { rooms: [] };
8853
8923
  },
8854
8924
  async getLiveRoomAttendance() {
8855
- throw new ApiError(501, "live rooms are not available in mock mode");
8925
+ throw new ApiError(501, "webinars are not available in mock mode");
8856
8926
  },
8857
8927
  async call(method, pathTemplate) {
8858
8928
  throw new ApiError(501, `${method} ${pathTemplate} is not available in mock mode`);
@@ -8890,6 +8960,9 @@ function createMockClient() {
8890
8960
  async getMetaAdDirectory() {
8891
8961
  throw new ApiError(501, "Meta Ads are not available in mock mode");
8892
8962
  },
8963
+ async listMetaAdConnections() {
8964
+ throw new ApiError(501, "Meta Ads are not available in mock mode");
8965
+ },
8893
8966
  async listMetaCampaigns() {
8894
8967
  throw new ApiError(501, "Meta Ads are not available in mock mode");
8895
8968
  },
@@ -9091,6 +9164,15 @@ function createHttpClient(config) {
9091
9164
  )
9092
9165
  );
9093
9166
  }
9167
+ if (tokenKind === "agent") {
9168
+ return Promise.reject(
9169
+ new ApiError(
9170
+ 403,
9171
+ "This command uses the Workspace API, which a community agent key can\u2019t access.",
9172
+ "An agent key reads the agent\u2019s communities and writes as the agent: use `zanfia communities \u2026`, or `zanfia login` for the workspace."
9173
+ )
9174
+ );
9175
+ }
9094
9176
  return requestFrom(config.apiUrl, method, path2, body);
9095
9177
  };
9096
9178
  const communityRequest = (method, path2, body) => {
@@ -9610,7 +9692,7 @@ function createHttpClient(config) {
9610
9692
  deleteMediaFolder: (name) => request("DELETE", `/media-folders/${encodeURIComponent(name)}`),
9611
9693
  initiateVideoUpload: (input) => request("POST", "/media/videos/initiate-upload", input),
9612
9694
  updatePage: (id, input) => request("PATCH", `/pages/${encodeURIComponent(id)}`, input),
9613
- publishPage: (id, publish) => request("POST", `/pages/${encodeURIComponent(id)}/publish`, { publish }),
9695
+ publishPage: (id, publish, options) => request("POST", `/pages/${encodeURIComponent(id)}/publish`, { publish, ...options }),
9614
9696
  deletePage: (id) => request("DELETE", `/pages/${encodeURIComponent(id)}`),
9615
9697
  setMainPage: (pageId) => request("PUT", "/pages/main", { pageId }),
9616
9698
  generatePage: (input) => request("POST", "/pages/generate", input),
@@ -9622,6 +9704,7 @@ function createHttpClient(config) {
9622
9704
  `/pages/${encodeURIComponent(id)}/revisions/${encodeURIComponent(revisionId)}/restore`
9623
9705
  ),
9624
9706
  activatePageAutomations: (id) => request("POST", `/pages/${encodeURIComponent(id)}/activate-automations`),
9707
+ startPageFamilyStep: (id, input) => request("POST", `/pages/${encodeURIComponent(id)}/family-step`, input),
9625
9708
  listSocialAccounts: () => request("GET", "/social/accounts"),
9626
9709
  listSocialPosts: (options) => {
9627
9710
  const params = new URLSearchParams();
@@ -9634,6 +9717,30 @@ function createHttpClient(config) {
9634
9717
  createSocialPost: (input) => request("POST", "/social/posts", input),
9635
9718
  scheduleSocialPost: (id, scheduledAt) => request("POST", `/social/posts/${encodeURIComponent(id)}/schedule`, { scheduledAt }),
9636
9719
  publishSocialPost: (id) => request("POST", `/social/posts/${encodeURIComponent(id)}/publish`),
9720
+ updateSocialPost: (id, input) => request("PATCH", `/social/posts/${encodeURIComponent(id)}`, input),
9721
+ unscheduleSocialPost: (id) => request("POST", `/social/posts/${encodeURIComponent(id)}/unschedule`),
9722
+ deleteSocialPost: (id) => request("DELETE", `/social/posts/${encodeURIComponent(id)}`),
9723
+ getYouTubeChannel: () => request("GET", "/social/youtube/channel"),
9724
+ getYouTubeChannelAnalytics: () => request("GET", "/social/youtube/analytics"),
9725
+ listYouTubeVideos: (options) => {
9726
+ const params = new URLSearchParams();
9727
+ if (options?.maxResults) params.set("maxResults", String(options.maxResults));
9728
+ if (options?.pageToken) params.set("pageToken", options.pageToken);
9729
+ const query = params.toString();
9730
+ return request("GET", query ? `/social/youtube/videos?${query}` : "/social/youtube/videos");
9731
+ },
9732
+ getYouTubeVideo: (videoId) => request("GET", `/social/youtube/videos/${encodeURIComponent(videoId)}`),
9733
+ getYouTubeVideoTranscript: (videoId) => request("GET", `/social/youtube/videos/${encodeURIComponent(videoId)}/transcript`),
9734
+ getInstagramInsights: () => request("GET", "/social/instagram/insights"),
9735
+ listInstagramMedia: (options) => {
9736
+ const params = new URLSearchParams();
9737
+ if (options?.limit) params.set("limit", String(options.limit));
9738
+ if (options?.after) params.set("after", options.after);
9739
+ const query = params.toString();
9740
+ return request("GET", query ? `/social/instagram/media?${query}` : "/social/instagram/media");
9741
+ },
9742
+ listThreadsPosts: () => request("GET", "/social/threads/posts"),
9743
+ getLatestSocialSnapshot: () => request("GET", "/social/snapshots/latest"),
9637
9744
  // Workspace team lives on the Workspace API (api-v1).
9638
9745
  listTeamMembers: () => request("GET", "/team/members"),
9639
9746
  listTeamInvitations: () => request("GET", "/team/invitations"),
@@ -9782,6 +9889,8 @@ function createHttpClient(config) {
9782
9889
  getDownloads: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/downloads`),
9783
9890
  updateDownloadsContent: (productId, editorContent) => request("PATCH", `/products/${encodeURIComponent(productId)}/downloads`, { editorContent }),
9784
9891
  addDownloadFiles: (productId, files) => request("POST", `/products/${encodeURIComponent(productId)}/downloads/files`, { files }),
9892
+ getProductFileSet: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/file-set`),
9893
+ changeProductFileSet: (productId, input) => request("POST", `/products/${encodeURIComponent(productId)}/file-set`, input),
9785
9894
  removeDownloadFile: (productId, uid) => request(
9786
9895
  "DELETE",
9787
9896
  `/products/${encodeURIComponent(productId)}/downloads/files/${encodeURIComponent(uid)}`
@@ -9976,6 +10085,10 @@ function createHttpClient(config) {
9976
10085
  "DELETE",
9977
10086
  `/v1/communities/${encodeURIComponent(communityId)}/events/${encodeURIComponent(eventId)}`
9978
10087
  ),
10088
+ listCommunityEventAttendees: (communityId, eventId) => communityRequest(
10089
+ "GET",
10090
+ `/v1/communities/${encodeURIComponent(communityId)}/events/${encodeURIComponent(eventId)}/attendees`
10091
+ ),
9979
10092
  getCommunityModerationQueue: (communityId) => communityRequest(
9980
10093
  "GET",
9981
10094
  `/v1/communities/${encodeURIComponent(communityId)}/moderation/reports`
@@ -10056,6 +10169,7 @@ function createHttpClient(config) {
10056
10169
  // Meta Ads live on the Workspace API (api-v1) under /meta-ads.
10057
10170
  getMetaAdsPerformance: (query) => request("GET", `/meta-ads/performance${toQuery(query)}`),
10058
10171
  getMetaAdDirectory: () => request("GET", "/meta-ads/directory"),
10172
+ listMetaAdConnections: () => request("GET", "/meta-ads/connections"),
10059
10173
  listMetaCampaigns: (scope, query) => request("GET", `${metaAdAccountPath(scope)}/campaigns${toQuery(query)}`),
10060
10174
  updateMetaCampaign: (scope, campaignId, input) => request(
10061
10175
  "PATCH",
@@ -10214,6 +10328,22 @@ function parseJsonObjectFileOrInline(flag) {
10214
10328
  return parsed;
10215
10329
  };
10216
10330
  }
10331
+ function parseJsonArrayFileOrInline(flag) {
10332
+ return (value) => {
10333
+ const trimmed = value.trim();
10334
+ const raw = trimmed.startsWith("[") ? trimmed : readFileSync2(value, "utf8");
10335
+ let parsed;
10336
+ try {
10337
+ parsed = JSON.parse(raw);
10338
+ } catch (error) {
10339
+ throw new Error(`Invalid JSON for ${flag}: ${error.message}`);
10340
+ }
10341
+ if (!Array.isArray(parsed)) {
10342
+ throw new Error(`${flag} must be a JSON array (inline or a .json file path).`);
10343
+ }
10344
+ return parsed;
10345
+ };
10346
+ }
10217
10347
  function requireConfirm(confirm, what) {
10218
10348
  if (!confirm) {
10219
10349
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
@@ -10595,17 +10725,96 @@ function registerCertificatesCommands(program2) {
10595
10725
  );
10596
10726
  emit(flags, result ?? {}, () => printJson(result ?? {}));
10597
10727
  });
10728
+ const certificatesSettings = certificates.command("settings");
10729
+ certificatesSettings.description(
10730
+ "A course's or event's certificate settings (auto-issue, layout, numbering, date, claim)"
10731
+ );
10732
+ certificatesSettings.command("get").summary("A product's certificate settings").description(
10733
+ 'Certificate settings of one course or event product \u2014 the same fields as the dashboard\'s product Settings \u2192 Certificate section: `certificateEnabled` (automatic certificate on course completion; courses only, always false for an event), `certificateTemplateId` (null = the workspace default template), `certificateCustomText` (the extra line printed by templates with the "custom text" element), `certificateValidityMonths` (null = never expires), numbering (`certificateCode`), the printed date (`certificateDateMode` + `certificateTrainingDate`) and claim mode (`certificateClaimEnabled` + `certificateClaimAvailableFrom`). 404 for an unknown product, 400 for a product type without certificates.'
10734
+ ).argument("<productId>", "Course or event product id (other product types answer 400).").action(async (productId, _options, command) => {
10735
+ const flags = getGlobalFlags(command);
10736
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10737
+ const params = {
10738
+ productId
10739
+ };
10740
+ const result = await client.call(
10741
+ "GET",
10742
+ "/products/:productId/certificate-settings",
10743
+ params,
10744
+ void 0,
10745
+ void 0
10746
+ );
10747
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10748
+ });
10749
+ certificatesSettings.command("update").summary("Update a product's certificate settings").description(
10750
+ "Partial update of one course or event product's certificate settings \u2014 what the dashboard's product Settings \u2192 Certificate section saves: automatic certificate on course completion (`certificateEnabled`, courses only), layout (`certificateTemplateId`), the extra line (`certificateCustomText`, max 300 characters / 3 lines), validity, numbering (`certificateCode`), the printed date (`certificateDateMode` + `certificateTrainingDate`) and claim mode (`certificateClaimEnabled` + `certificateClaimAvailableFrom`). Absent fields stay unchanged, null clears; an empty body is a 400. The whole patch is checked before anything is saved (409 taken code, 404 unknown product or template, 400 invalid value). Only certificates issued from now on use the new settings. Answers the settings as saved."
10751
+ ).argument("<productId>", "Course or event product id (other product types answer 400).").option(
10752
+ "--auto-issue",
10753
+ "Automatic certificate on course completion. Course products only (400 on an event)."
10754
+ ).option("--no-auto-issue", "Send `certificateEnabled: false` explicitly.").option(
10755
+ "--template <value>",
10756
+ "Certificate template id from list-certificate-templates (404 when not in this workspace); null or blank = back to the workspace default."
10757
+ ).option(
10758
+ "--custom-text <value>",
10759
+ 'Extra line printed by templates with the "custom text" element, e.g. training hours. Max 300 characters and 3 lines (line breaks kept); null or blank clears it.'
10760
+ ).option(
10761
+ "--validity-months <n>",
10762
+ "Validity in months from issuance; null = never expires.",
10763
+ parseNumberOption("--validity-months", NumberFormat.Integer)
10764
+ ).option(
10765
+ "--code <value>",
10766
+ "Exactly 3 digits (`017`), unique in the workspace (409 when another product has it); numbers certificates `{seq}/{code}/{schoolYear}`. null or blank = unnumbered."
10767
+ ).addOption(
10768
+ new Option(
10769
+ "--date-mode <completion|training_date|issue_date>",
10770
+ "Printed date: `completion` (course progress, else the issue date), `training_date` (needs certificateTrainingDate) or `issue_date`. null = completion."
10771
+ ).choices(["completion", "training_date", "issue_date"])
10772
+ ).option(
10773
+ "--training-date <value>",
10774
+ "`YYYY-MM-DD`, printed in the `training_date` mode; null or blank clears."
10775
+ ).option("--claim", "Participants claim the certificate themselves in the portal (claim mode).").option("--no-claim", "Send `certificateClaimEnabled: false` explicitly.").option(
10776
+ "--claim-from <value>",
10777
+ "First claim day `YYYY-MM-DD`; null or blank = the day after the training date (else at once)."
10778
+ ).action(
10779
+ async (productId, options, command) => {
10780
+ const flags = getGlobalFlags(command);
10781
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10782
+ const params = {
10783
+ productId
10784
+ };
10785
+ const payload = {
10786
+ certificateEnabled: options.autoIssue,
10787
+ certificateTemplateId: options.template,
10788
+ certificateCustomText: options.customText,
10789
+ certificateValidityMonths: options.validityMonths,
10790
+ certificateCode: options.code,
10791
+ certificateDateMode: options.dateMode,
10792
+ certificateTrainingDate: options.trainingDate,
10793
+ certificateClaimEnabled: options.claim,
10794
+ certificateClaimAvailableFrom: options.claimFrom
10795
+ };
10796
+ const result = await client.call(
10797
+ "PATCH",
10798
+ "/products/:productId/certificate-settings",
10799
+ params,
10800
+ void 0,
10801
+ payload
10802
+ );
10803
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10804
+ }
10805
+ );
10598
10806
  }
10599
10807
 
10600
10808
  // src/commands/_generated/blog.ts
10601
10809
  var listBlogPostsColumns = ["id", "slug", "title", "status", "publishedAt", "updatedAt"];
10810
+ var listBlogCategoriesColumns = ["id", "name", "slug", "postCount"];
10602
10811
  function registerBlogCommands(program2) {
10603
10812
  const blog = program2.command("blog");
10604
10813
  blog.description(
10605
10814
  "The workspace blog (Your site \u2192 Blog): Markdown posts served at /blog on the store host; drafts are visible to the team only"
10606
10815
  );
10607
10816
  blog.command("list").summary("List blog posts").description(
10608
- "Every post of the workspace blog (drafts and published), newest change first: id, slug (`/blog/{slug}` on the store host), title, excerpt, cover, tags, status and dates. No body - use get-blog-post for the Markdown. The id source for every other blog tool."
10817
+ "Every post of the workspace blog (drafts and published), newest change first: id, slug (`/blog/{slug}` on the store host), title, excerpt, cover, tags, status and dates. No body: use get-blog-post for the Markdown. The id source for every other blog tool."
10609
10818
  ).action(async (_options, command) => {
10610
10819
  const flags = getGlobalFlags(command);
10611
10820
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -10615,14 +10824,18 @@ function registerBlogCommands(program2) {
10615
10824
  emit(flags, result ?? {}, () => printTable(rows, listBlogPostsColumns));
10616
10825
  });
10617
10826
  blog.command("create").summary("Create a blog post").description(
10618
- "Creates a blog post as a DRAFT (or published with `publish: true` when it has a body). The body is Markdown. A slug the caller sets must be free (409) and not `feed`; a slug derived from the title takes the next free numeric suffix. Returns the full post. Requires the blog early access on the workspace (403 otherwise)."
10827
+ "Creates a blog post as a DRAFT (or published with `publish: true` when it has a body). The body is Markdown. A slug the caller sets must be free (409) and not `feed`; a slug derived from the title takes the next free numeric suffix. `categoryIds` puts it in up to 3 blog categories (list-blog-categories). To go live later, create the draft and call schedule-blog-post. Returns the full post."
10619
10828
  ).requiredOption("--title <value>", "Post title (max 200 characters).").option(
10620
10829
  "--slug <value>",
10621
10830
  "Derived from the title when omitted; a taken derived slug gets a numeric suffix."
10622
10831
  ).option("--content <value>", "The body as Markdown (headings, lists, links, images by URL).").option(
10623
10832
  "--excerpt <value>",
10624
10833
  "Short plain-text summary (list, RSS, meta description); derived from the body on publish when empty."
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(
10834
+ ).option("--cover <value>", "Cover image URL (Media Library).").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option(
10835
+ "--category-ids <a,b,...>",
10836
+ "Ids of the blog categories (`bcat_\u2026` from list-blog-categories) the post sits in, at most 3; replaces the whole list, [] = none. Tags stay a separate label.",
10837
+ parseCommaList("--category-ids")
10838
+ ).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
10839
  "--published-at <value>",
10627
10840
  "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
10841
  ).action(async (options, command) => {
@@ -10636,6 +10849,7 @@ function registerBlogCommands(program2) {
10636
10849
  excerpt: options.excerpt,
10637
10850
  coverImageUrl: options.cover,
10638
10851
  tags: options.tags,
10852
+ categoryIds: options.categoryIds,
10639
10853
  seo: options.seo,
10640
10854
  publish: options.publish,
10641
10855
  publishedAt: options.publishedAt
@@ -10655,7 +10869,7 @@ function registerBlogCommands(program2) {
10655
10869
  emit(flags, result ?? {}, () => printJson(result ?? {}));
10656
10870
  });
10657
10871
  blog.command("update").summary("Update a blog post").description(
10658
- "Partial update of a blog post: title, slug (must be free, 409), Markdown body, excerpt, cover (null removes it), tags, SEO (whole section replaced). A published post keeps serving the new content at once. Returns the full post."
10872
+ "Partial update of a blog post: title, slug (must be free, 409), Markdown body, excerpt, cover (null removes it), tags, categories (`categoryIds`, the whole list, at most 3), SEO (whole section replaced). A published post keeps serving the new content at once; a scheduled one stays scheduled. Returns the full post."
10659
10873
  ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--title <value>", "title").option(
10660
10874
  "--slug <value>",
10661
10875
  "Public path segment (`/blog/{slug}`): lowercase letters, digits and hyphens."
@@ -10663,6 +10877,10 @@ function registerBlogCommands(program2) {
10663
10877
  "--excerpt <value>",
10664
10878
  "Short plain-text summary (list, RSS, meta description); derived from the body on publish when empty."
10665
10879
  ).option("--cover <value>", "Cover image URL; null removes the cover.").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option(
10880
+ "--category-ids <a,b,...>",
10881
+ "Ids of the blog categories (`bcat_\u2026` from list-blog-categories) the post sits in, at most 3; replaces the whole list, [] = none. Tags stay a separate label.",
10882
+ parseCommaList("--category-ids")
10883
+ ).option(
10666
10884
  "--seo <json|file>",
10667
10885
  "Replaces the whole SEO section.",
10668
10886
  parseJsonObjectFileOrInline("--seo")
@@ -10682,6 +10900,7 @@ function registerBlogCommands(program2) {
10682
10900
  excerpt: options.excerpt,
10683
10901
  coverImageUrl: options.cover,
10684
10902
  tags: options.tags,
10903
+ categoryIds: options.categoryIds,
10685
10904
  seo: options.seo,
10686
10905
  publishedAt: options.publishedAt
10687
10906
  };
@@ -10689,8 +10908,8 @@ function registerBlogCommands(program2) {
10689
10908
  emit(flags, result ?? {}, () => printJson(result ?? {}));
10690
10909
  });
10691
10910
  blog.command("publish").summary("Publish or unpublish a blog post").description(
10692
- "Publishes a post at `/blog/{slug}` (needs a non-empty body, 400 otherwise; the excerpt is derived from the body when empty) or takes it back to draft. The first publication date is the post's date and survives unpublish / republish. Reversible. Returns the full post."
10693
- ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--publish", "true = publish, false = back to draft.").action(async (id, options, command) => {
10911
+ "Publishes a post at `/blog/{slug}` now (needs a non-empty body, 400 otherwise; the excerpt is derived from the body when empty) or takes it back to draft. A scheduled post is published at once and its schedule dropped; back to draft drops it too. The first publication date is the post's date and survives unpublish / republish. Reversible. Without `publish` (an empty request body) the post is published. Returns the full post."
10912
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--publish", "true = publish (the default when absent), false = back to draft.").option("--no-publish", "Send `publish: false` explicitly.").action(async (id, options, command) => {
10694
10913
  const flags = getGlobalFlags(command);
10695
10914
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10696
10915
  const params = {
@@ -10709,7 +10928,7 @@ function registerBlogCommands(program2) {
10709
10928
  emit(flags, result ?? {}, () => printJson(result ?? {}));
10710
10929
  });
10711
10930
  blog.command("delete").summary("Delete a blog post").description(
10712
- "Deletes a blog post for good; a published one disappears from `/blog` at once. No undo - prefer unpublishing."
10931
+ "Deletes a blog post for good; a published one disappears from `/blog` at once. No undo; prefer unpublishing."
10713
10932
  ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--confirm", "required \u2014 acknowledge that you want to deletes the post for good").action(async (id, options, command) => {
10714
10933
  const flags = getGlobalFlags(command);
10715
10934
  requireConfirm(options.confirm, "deletes the post for good");
@@ -10726,6 +10945,118 @@ function registerBlogCommands(program2) {
10726
10945
  );
10727
10946
  emit(flags, result ?? {}, () => printJson(result ?? {}));
10728
10947
  });
10948
+ blog.command("schedule").summary("Schedule a blog post").description(
10949
+ "Schedules a DRAFT (or reschedules a scheduled post) to be published at `scheduledAt`: status `scheduled`, not public anywhere (list, feed, sitemap, its URL) until then, then published exactly like publish-blog-post. Needs a non-empty body (400); a published post answers 409 (unpublish it first). `scheduledAt: null` unschedules (back to draft); publishing it now is publish-blog-post. Returns the full post with `scheduledAt`."
10950
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").requiredOption(
10951
+ "--at <value>",
10952
+ `When the post goes live: an ISO 8601 date-time WITH the offset of the creator's time zone, Europe/Warsaw = +01:00 in winter (CET), +02:00 in summer (CEST), e.g. "poniedzia\u0142ek 8:00" in late October = 2026-10-26T08:00:00+01:00. At least a minute and at most a year ahead. null = unschedule (back to draft).`
10953
+ ).action(async (id, options, command) => {
10954
+ const flags = getGlobalFlags(command);
10955
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10956
+ const params = {
10957
+ postId: id
10958
+ };
10959
+ const payload = {
10960
+ scheduledAt: options.at
10961
+ };
10962
+ const result = await client.call(
10963
+ "POST",
10964
+ "/blog-posts/:postId/schedule",
10965
+ params,
10966
+ void 0,
10967
+ payload
10968
+ );
10969
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10970
+ });
10971
+ const blogCategories = blog.command("categories");
10972
+ blogCategories.description(
10973
+ "Categories of the workspace blog: a post sits in up to 3, each has its own page"
10974
+ );
10975
+ blogCategories.command("list").summary("List blog categories").description(
10976
+ "The categories of the workspace blog, by name: id, name, slug (public page `/blog/kategoria/{slug}`, `/blog/category/{slug}` on English sites), description, SEO and how many posts sit in each. The id source for `categoryIds` on posts."
10977
+ ).action(async (_options, command) => {
10978
+ const flags = getGlobalFlags(command);
10979
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10980
+ const params = {};
10981
+ const result = await client.call("GET", "/blog-categories", params, void 0, void 0);
10982
+ const rows = tableRowsOf(result, "categories");
10983
+ emit(flags, result ?? {}, () => printTable(rows, listBlogCategoriesColumns));
10984
+ });
10985
+ blogCategories.command("create").summary("Create a blog category").description(
10986
+ "Adds a category to the blog (at most 50). A slug the caller sets must be free (409); one derived from the name takes the next free numeric suffix. The category page goes public once a published post sits in it. Returns the category."
10987
+ ).argument("<name>", 'Category name (max 60 characters), e.g. "Poradniki".').option(
10988
+ "--slug <value>",
10989
+ "Derived from the name when omitted; a taken derived slug gets a numeric suffix."
10990
+ ).option(
10991
+ "--description <value>",
10992
+ "Plain text shown under the category heading; the default meta description."
10993
+ ).option("--seo <json|file>", "seo", parseJsonObjectFileOrInline("--seo")).action(async (name, options, command) => {
10994
+ const flags = getGlobalFlags(command);
10995
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10996
+ const params = {};
10997
+ const payload = {
10998
+ name,
10999
+ slug: options.slug,
11000
+ description: options.description,
11001
+ seo: options.seo
11002
+ };
11003
+ const result = await client.call("POST", "/blog-categories", params, void 0, payload);
11004
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11005
+ });
11006
+ blogCategories.command("update").summary("Update a blog category").description(
11007
+ "Partial update of a category: name, slug (must be free, 409; a new slug moves the public category page and the old address stops working), description, SEO (whole section replaced). Its posts stay in it. Returns the category."
11008
+ ).argument("<id>", "Blog category id (`bcat_\u2026`) from list-blog-categories.").option("--name <value>", "name").option(
11009
+ "--slug <value>",
11010
+ "Public path segment of the category page (`/blog/kategoria/{slug}`, `/blog/category/{slug}` on English sites): lowercase letters, digits and hyphens."
11011
+ ).option(
11012
+ "--description <value>",
11013
+ "Plain text shown under the category heading; the default meta description."
11014
+ ).option(
11015
+ "--seo <json|file>",
11016
+ "Replaces the whole SEO section.",
11017
+ parseJsonObjectFileOrInline("--seo")
11018
+ ).action(async (id, options, command) => {
11019
+ const flags = getGlobalFlags(command);
11020
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11021
+ const params = {
11022
+ categoryId: id
11023
+ };
11024
+ const payload = {
11025
+ name: options.name,
11026
+ slug: options.slug,
11027
+ description: options.description,
11028
+ seo: options.seo
11029
+ };
11030
+ const result = await client.call(
11031
+ "PATCH",
11032
+ "/blog-categories/:categoryId",
11033
+ params,
11034
+ void 0,
11035
+ payload
11036
+ );
11037
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11038
+ });
11039
+ blogCategories.command("delete").summary("Delete a blog category").description(
11040
+ "Deletes a category for good and takes it off every post in it; the posts stay as they are and the category page stops working. No undo."
11041
+ ).argument("<id>", "Blog category id (`bcat_\u2026`) from list-blog-categories.").option(
11042
+ "--confirm",
11043
+ "required \u2014 acknowledge that you want to deletes the category for good (its posts stay)"
11044
+ ).action(async (id, options, command) => {
11045
+ const flags = getGlobalFlags(command);
11046
+ requireConfirm(options.confirm, "deletes the category for good (its posts stay)");
11047
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11048
+ const params = {
11049
+ categoryId: id
11050
+ };
11051
+ const result = await client.call(
11052
+ "DELETE",
11053
+ "/blog-categories/:categoryId",
11054
+ params,
11055
+ void 0,
11056
+ void 0
11057
+ );
11058
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11059
+ });
10729
11060
  }
10730
11061
 
10731
11062
  // src/commands/_generated/site-redirects.ts
@@ -10813,10 +11144,10 @@ var listPopupsColumns = ["id", "name", "status", "priority", "updatedAt"];
10813
11144
  function registerPopupsCommands(program2) {
10814
11145
  const popups = program2.command("popups");
10815
11146
  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"
11147
+ "Pop-ups on the workspace site (Your site \u2192 Pop-ups): a signup form, an offer or a link shown on entry, after scrolling or on exit intent, after the cookie decision, never again after a conversion"
10817
11148
  );
10818
11149
  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."
11150
+ "Every pop-up of the workspace site (drafts, active, paused) in priority order: copy, content (form, offer or link), layout, triggers per device, targeting, frequency and schedule. The id source for every other pop-up tool."
10820
11151
  ).action(async (_options, command) => {
10821
11152
  const flags = getGlobalFlags(command);
10822
11153
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -10826,7 +11157,7 @@ function registerPopupsCommands(program2) {
10826
11157
  emit(flags, result ?? {}, () => printTable(rows, listPopupsColumns));
10827
11158
  });
10828
11159
  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)."
11160
+ "Creates a pop-up as a DRAFT. Content: a signup form (default; without `content.formId` a new standalone signup form named after the pop-up is created, visible in Forms), an offer (a checkout of a product; the button opens it) or a link button. Defaults: a centred card (`design.layout: slide-in` = a corner card), desktop after 10 s on the page (or exit intent), phones after 40 % scroll as a bottom sheet, again after 7 days, all hosted pages except thank-you / download / member pages (or the blog / the store), everyone (or new / returning visitors), hidden for subscribers. It is turned on with set-popup-status (in Zanfia\u2019s own chat agent, behind an approval card the creator confirms) once it has a title and a button text. Requires the pop-ups feature on the plan (403 otherwise)."
10830
11161
  ).requiredOption(
10831
11162
  "--name <value>",
10832
11163
  "Internal name (panel only); also the name of a provisioned form."
@@ -10845,7 +11176,7 @@ function registerPopupsCommands(program2) {
10845
11176
  parseJsonObjectFileOrInline("--copy")
10846
11177
  ).option(
10847
11178
  "--content <json|file>",
10848
- "What the pop-up holds: a signup form.",
11179
+ "What the pop-up holds: a signup form, an offer (koszyk) or a link button.",
10849
11180
  parseJsonObjectFileOrInline("--content")
10850
11181
  ).option("--design <json|file>", "design", parseJsonObjectFileOrInline("--design")).option(
10851
11182
  "--trigger <json|file>",
@@ -10883,7 +11214,7 @@ function registerPopupsCommands(program2) {
10883
11214
  emit(flags, result ?? {}, () => printJson(result ?? {}));
10884
11215
  });
10885
11216
  popups.command("get").summary("Pop-up detail").description(
10886
- "One pop-up in full: status, copy, form content, triggers, targeting, frequency, schedule."
11217
+ "One pop-up in full: status, copy, content (form, offer or link), design, triggers, targeting, frequency, schedule."
10887
11218
  ).argument("<id>", "Pop-up id (`popup_\u2026`) from list-popups.").action(async (id, _options, command) => {
10888
11219
  const flags = getGlobalFlags(command);
10889
11220
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -10894,7 +11225,7 @@ function registerPopupsCommands(program2) {
10894
11225
  emit(flags, result ?? {}, () => printJson(result ?? {}));
10895
11226
  });
10896
11227
  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."
11228
+ "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; another content kind replaces the content). An active pop-up must keep its title, button and its form, offer or link (400 popup-incomplete). Live sites pick changes up within a few minutes."
10898
11229
  ).argument("<id>", "Pop-up id (`popup_\u2026`) from list-popups.").option("--name <value>", "Internal name (panel only).").option(
10899
11230
  "--priority <n>",
10900
11231
  "Lower shows first when several pop-ups match a page; default = after the others.",
@@ -10910,7 +11241,7 @@ function registerPopupsCommands(program2) {
10910
11241
  parseJsonObjectFileOrInline("--copy")
10911
11242
  ).option(
10912
11243
  "--content <json|file>",
10913
- "What the pop-up holds: a signup form.",
11244
+ "What the pop-up holds: a signup form, an offer (koszyk) or a link button.",
10914
11245
  parseJsonObjectFileOrInline("--content")
10915
11246
  ).option("--design <json|file>", "design", parseJsonObjectFileOrInline("--design")).option(
10916
11247
  "--trigger <json|file>",
@@ -10984,6 +11315,318 @@ function registerPopupsCommands(program2) {
10984
11315
  const result = await client.call("DELETE", "/popups/:popupId", params, void 0, void 0);
10985
11316
  emit(flags, result ?? {}, () => printJson(result ?? {}));
10986
11317
  });
11318
+ popups.command("stats").summary("Pop-up stats").description(
11319
+ "Views, closes, conversions and conversion % of one pop-up, day by day for the range (default the last 30 days), and split by device and page (`{surface}~{pageId}`) for the months the range touches. A conversion is a first signup through the form, or a click on the offer or link button."
11320
+ ).argument("<id>", "Pop-up id (`popup_\u2026`) from list-popups.").option(
11321
+ "--from <value>",
11322
+ "First day `YYYY-MM-DD` (UTC, inclusive); default 29 days before `to`."
11323
+ ).option(
11324
+ "--to <value>",
11325
+ "Last day `YYYY-MM-DD` (UTC, inclusive); default today. At most 366 days."
11326
+ ).action(async (id, options, command) => {
11327
+ const flags = getGlobalFlags(command);
11328
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11329
+ const params = {
11330
+ popupId: id
11331
+ };
11332
+ const payload = {
11333
+ from: options.from,
11334
+ to: options.to
11335
+ };
11336
+ const result = await client.call("GET", "/popups/:popupId/stats", params, payload, void 0);
11337
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11338
+ });
11339
+ }
11340
+
11341
+ // src/commands/_generated/studio.ts
11342
+ var suggestStudioVideoClipsColumns = ["startSeconds", "endSeconds", "hook"];
11343
+ function registerStudioCommands(program2) {
11344
+ const studio = program2.commands.find((command) => command.name() === "studio") ?? program2.command("studio");
11345
+ if (!studio.description()) {
11346
+ studio.description(
11347
+ "Studio videos: suggest clips, write a reel storyboard, voice it, export and download"
11348
+ );
11349
+ }
11350
+ const studioVideo = studio.command("video");
11351
+ studioVideo.description(
11352
+ "Studio videos: suggest clips, write a reel storyboard, voice it, export and download"
11353
+ );
11354
+ studioVideo.command("quote").summary("Quote what a Studio video costs").description(
11355
+ "Before spending anything: the AI credits of each Studio video step (clip suggestion, storyboard, voice-over for a reel of `lengthSeconds`), priced as the panel prices them, and the plan's monthly video exports left (the render costs no credits but uses one). Calls no model. Read-only."
11356
+ ).option(
11357
+ "--length-seconds <n>",
11358
+ "Reel length in seconds of voice: 15, 30 or 45. Default 30.",
11359
+ parseNumberOption("--length-seconds", NumberFormat.Integer)
11360
+ ).action(async (options, command) => {
11361
+ const flags = getGlobalFlags(command);
11362
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11363
+ const params = {};
11364
+ const payload = {
11365
+ lengthSeconds: options.lengthSeconds
11366
+ };
11367
+ const result = await client.call("GET", "/studio/videos/quote", params, payload, void 0);
11368
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11369
+ });
11370
+ studioVideo.command("suggest").summary("Suggest reel clips from a video transcript").description(
11371
+ "Reads the transcript of one library video and proposes short clips (hook, start / end seconds, why it works) for a MomentClip export. Spends AI credits (see get-studio-video-quote `suggestClipsCredits`; 402 ai-credits-exhausted when they are out). Stores nothing. The video must be transcribed first (400 clip-suggestion-transcript-unavailable)."
11372
+ ).argument(
11373
+ "<mediaId>",
11374
+ "A transcribed video: a lesson, a LIVE recording or any library video (a media-library id from list-media)."
11375
+ ).option(
11376
+ "--language <value>",
11377
+ "Language of the text, e.g. `pl` or `en`. Omitted: the transcript language, else `pl`."
11378
+ ).action(async (mediaId, options, command) => {
11379
+ const flags = getGlobalFlags(command);
11380
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11381
+ const params = {};
11382
+ const payload = {
11383
+ mediaId,
11384
+ language: options.language
11385
+ };
11386
+ const result = await client.call(
11387
+ "POST",
11388
+ "/studio/videos/clip-suggestions",
11389
+ params,
11390
+ void 0,
11391
+ payload
11392
+ );
11393
+ const rows = tableRowsOf(result, "clips");
11394
+ emit(flags, result ?? {}, () => printTable(rows, suggestStudioVideoClipsColumns));
11395
+ });
11396
+ studioVideo.command("storyboard").summary("Write a reel storyboard (ScriptReel)").description(
11397
+ "\u201ENapisz scenariusz\u201D of \u201EWygeneruj rolk\u0119\u201D: 3-6 scenes (on-screen text + what the voice says) for a reel of `lengthSeconds`, about `topic` or grounded in a transcribed video. Spends AI credits (get-studio-video-quote `storyboardCredits`; 402 ai-credits-exhausted). Stores nothing: pass the scenes to generate-studio-video-voiceover and export-studio-video. 422 script-reel-storyboard-failed = the model answered unusably (free): try again."
11398
+ ).option("--topic <value>", "What the reel is about; required unless `sourceMediaId`.").option(
11399
+ "--source-media-id <value>",
11400
+ "A transcribed library video (lesson, LIVE) whose transcript grounds the script."
11401
+ ).option(
11402
+ "--length-seconds <n>",
11403
+ "Reel length in seconds of voice: 15, 30 or 45. Default 30.",
11404
+ parseNumberOption("--length-seconds", NumberFormat.Integer)
11405
+ ).option(
11406
+ "--language <value>",
11407
+ "Language of the text, e.g. `pl` or `en`. Omitted: the transcript language, else `pl`."
11408
+ ).action(async (options, command) => {
11409
+ const flags = getGlobalFlags(command);
11410
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11411
+ const params = {};
11412
+ const payload = {
11413
+ topic: options.topic,
11414
+ sourceMediaId: options.sourceMediaId,
11415
+ lengthSeconds: options.lengthSeconds,
11416
+ language: options.language
11417
+ };
11418
+ const result = await client.call(
11419
+ "POST",
11420
+ "/studio/videos/storyboards",
11421
+ params,
11422
+ void 0,
11423
+ payload
11424
+ );
11425
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11426
+ });
11427
+ studioVideo.command("voiceover").summary("Generate the voice-over of a reel storyboard").description(
11428
+ "\u201EWygeneruj g\u0142os\u201D for a storyboard: ONE voice reading every scene's voice text, stored as an Audio item in the media library. AI credits per second of the voice, charged only once it is stored (any failure is free); 402 ai-credits-exhausted before anything is made. Pass the answer's `mediaId` as `voiceoverMediaId` to export-studio-video with the SAME scenes (changed scenes = 409 script-reel-voice-mismatch)."
11429
+ ).requiredOption(
11430
+ "--scenes <json|file>",
11431
+ "The storyboard scenes from write-studio-video-storyboard (edited or not), in order. The voice and the export must use the SAME scenes.",
11432
+ parseJsonArrayFileOrInline("--scenes")
11433
+ ).addOption(
11434
+ new Option(
11435
+ "--voice-id <asia|iwona|pawel|piotr>",
11436
+ "Voice: asia, iwona (female), pawel, piotr (male). Default asia."
11437
+ ).choices(["asia", "iwona", "pawel", "piotr"])
11438
+ ).option(
11439
+ "--language <value>",
11440
+ "Language of the text, e.g. `pl` or `en`. Omitted: the transcript language, else `pl`."
11441
+ ).option(
11442
+ "--request-id <value>",
11443
+ "Idempotency key (a uuid): a retry with the same one returns the same voice, charged once. Omitted = a new voice every call."
11444
+ ).action(async (options, command) => {
11445
+ const flags = getGlobalFlags(command);
11446
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11447
+ const params = {};
11448
+ const payload = {
11449
+ scenes: options.scenes,
11450
+ voiceId: options.voiceId,
11451
+ language: options.language,
11452
+ requestId: options.requestId
11453
+ };
11454
+ const result = await client.call(
11455
+ "POST",
11456
+ "/studio/videos/voiceovers",
11457
+ params,
11458
+ void 0,
11459
+ payload
11460
+ );
11461
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11462
+ });
11463
+ studioVideo.command("export").summary("Export (render) a Studio video").description(
11464
+ "Starts the render of a MomentClip, ScriptReel, Montage or CoursePromo video, the same render the panel runs; the MP4 lands in the media library. No AI credits, but it uses one of the plan's monthly video exports (429 promo-video-quota-exceeded when none are left; get-studio-video-quote `exports`). Answers a job id at once (202): poll get-studio-video-status. Publishes nothing."
11465
+ ).addOption(
11466
+ new Option(
11467
+ "--template-id <MomentClip|ScriptReel|Montage|CoursePromo>",
11468
+ "MomentClip = a captioned clip of a transcribed video (`moment`); ScriptReel = the storyboard reel (`scenes` + `voiceoverMediaId`); Montage = library pictures / videos (`montageScenes`); CoursePromo = a course promo (`productId`)."
11469
+ ).choices(["MomentClip", "ScriptReel", "Montage", "CoursePromo"]).makeOptionMandatory()
11470
+ ).option(
11471
+ "--moment <json|file>",
11472
+ "MomentClip: the segment, from suggest-studio-video-clips or chosen by hand.",
11473
+ parseJsonObjectFileOrInline("--moment")
11474
+ ).option(
11475
+ "--scenes <json|file>",
11476
+ "ScriptReel: the SAME scenes the voice was generated for.",
11477
+ parseJsonArrayFileOrInline("--scenes")
11478
+ ).option(
11479
+ "--voiceover-media-id <value>",
11480
+ "ScriptReel: `mediaId` from generate-studio-video-voiceover."
11481
+ ).option(
11482
+ "--montage-scenes <json|file>",
11483
+ "Montage: 1-12 library images / videos in order.",
11484
+ parseJsonArrayFileOrInline("--montage-scenes")
11485
+ ).option("--product-id <value>", "CoursePromo: the course (`prod_\u2026` from list-products).").addOption(new Option("--locale <en|pl>", "locale").choices(["en", "pl"])).addOption(
11486
+ new Option("--style <midnight|editorial|bold>", "style").choices(["midnight", "editorial", "bold"])
11487
+ ).option(
11488
+ "--music-track-id <value>",
11489
+ "A music track id from the panel manifest; omitted = no music."
11490
+ ).addOption(
11491
+ new Option(
11492
+ "--format <9:16|1:1|16:9>",
11493
+ "Aspect ratio; default 9:16 (MomentClip / ScriptReel are always 9:16)."
11494
+ ).choices(["9:16", "1:1", "16:9"])
11495
+ ).option("--cta-label <value>", "ctaLabel").option("--include-cover-image", "includeCoverImage").option("--no-include-cover-image", "Send `includeCoverImage: false` explicitly.").option("--include-social-proof", "includeSocialProof").option("--no-include-social-proof", "Send `includeSocialProof: false` explicitly.").option("--include-testimonial", "includeTestimonial").option("--no-include-testimonial", "Send `includeTestimonial: false` explicitly.").option("--include-logo", "includeLogo").option("--no-include-logo", "Send `includeLogo: false` explicitly.").action(async (options, command) => {
11496
+ const flags = getGlobalFlags(command);
11497
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11498
+ const params = {};
11499
+ const payload = {
11500
+ templateId: options.templateId,
11501
+ moment: options.moment,
11502
+ scenes: options.scenes,
11503
+ voiceoverMediaId: options.voiceoverMediaId,
11504
+ montageScenes: options.montageScenes,
11505
+ productId: options.productId,
11506
+ locale: options.locale,
11507
+ style: options.style,
11508
+ musicTrackId: options.musicTrackId,
11509
+ format: options.format,
11510
+ ctaLabel: options.ctaLabel,
11511
+ includeCoverImage: options.includeCoverImage,
11512
+ includeSocialProof: options.includeSocialProof,
11513
+ includeTestimonial: options.includeTestimonial,
11514
+ includeLogo: options.includeLogo
11515
+ };
11516
+ const result = await client.call(
11517
+ "POST",
11518
+ "/studio/videos/exports",
11519
+ params,
11520
+ void 0,
11521
+ payload
11522
+ );
11523
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11524
+ });
11525
+ studioVideo.command("status").summary("Read the status of a Studio video export").description(
11526
+ "The render status of an export-studio-video job; once done, the media-library item it landed (its id attaches to a social post draft via `mediaIds`) and, once the video encoded, a signed MP4 download link valid for 24 hours (ask again for a fresh one). Poll every 15-30 s; a render takes a minute or a few. Read-only."
11527
+ ).argument("<id>", "The job id export-studio-video answered.").action(async (id, _options, command) => {
11528
+ const flags = getGlobalFlags(command);
11529
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11530
+ const params = {
11531
+ jobId: id
11532
+ };
11533
+ const result = await client.call(
11534
+ "GET",
11535
+ "/studio/videos/exports/:jobId",
11536
+ params,
11537
+ void 0,
11538
+ void 0
11539
+ );
11540
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11541
+ });
11542
+ }
11543
+
11544
+ // src/commands/_generated/site.ts
11545
+ var listSiteTemplatePagesColumns = ["pageId", "title", "slug", "issue", "nav"];
11546
+ var getSiteMenuColumns = ["label", "style", "href"];
11547
+ function registerSiteCommands(program2) {
11548
+ const site = program2.commands.find((command) => command.name() === "site") ?? program2.command("site");
11549
+ if (!site.description()) {
11550
+ site.description(
11551
+ "What frames the blog (Your site \u2192 Look and menu): the storefront shell or one of your pages; checked by the same rules as the dashboard"
11552
+ );
11553
+ }
11554
+ const siteLayout = site.command("layout");
11555
+ siteLayout.description(
11556
+ "What frames the blog (Your site \u2192 Look and menu): the storefront shell or one of your pages; checked by the same rules as the dashboard"
11557
+ );
11558
+ siteLayout.command("get").summary("Get the site layout").description(
11559
+ 'What frames the blog and the shared site menu (dashboard: Your site \u2192 Look and menu). `template` = `{ kind: "storefront" }` (the storefront shell, the default) or `{ kind: "page", pageId }` (the blog wears that page\'s header, menu and footer: \u201EJak moja strona\u201D). `menu` = the shared menu in order, each entry with `href`, the address it leads to today (null = it leads nowhere now, pages leave it out); an empty menu = every page keeps its own links.'
11560
+ ).action(async (_options, command) => {
11561
+ const flags = getGlobalFlags(command);
11562
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11563
+ const params = {};
11564
+ const result = await client.call("GET", "/site/layout", params, void 0, void 0);
11565
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11566
+ });
11567
+ siteLayout.command("pages").summary("List pages that can frame the blog").description(
11568
+ "The workspace's published pages as candidates for the blog's frame, eligible ones first. `issue` null = the page can frame the blog (pass its `pageId` to set-site-template); otherwise why not: `not-published`, `gated` (behind a product or a deadline), `custom-scripts` (its own JS), `no-header-or-footer`. `nav` = whether the shared menu shows on the page: `shared`, `not-marked` (no `data-zanfia-nav`, its own links stay) or `unsupported`."
11569
+ ).action(async (_options, command) => {
11570
+ const flags = getGlobalFlags(command);
11571
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11572
+ const params = {};
11573
+ const result = await client.call(
11574
+ "GET",
11575
+ "/site/layout/template-pages",
11576
+ params,
11577
+ void 0,
11578
+ void 0
11579
+ );
11580
+ const rows = tableRowsOf(result, "pages");
11581
+ emit(flags, result ?? {}, () => printTable(rows, listSiteTemplatePagesColumns));
11582
+ });
11583
+ siteLayout.command("set").summary("Set what frames the blog").description(
11584
+ "Sets what frames the blog (\u201EWygl\u0105d bloga\u201D): `page` + `pageId` = the blog wears that page's header, menu, footer, fonts and colours (\u201EJak moja strona\u201D); `storefront` = the storefront shell. Live at once; the shared menu stays as it is. The page must be published, open to everyone, without its own scripts and with a <header> and a <footer>: 400 `site-template-page-ineligible` with `issue` otherwise, 404 `site-template-page-not-found`, 400 `site-template-page-required` for `page` without `pageId`; 403 `feature_not_available` while the blog is not on the plan. Returns the whole layout."
11585
+ ).argument(
11586
+ "<kind>",
11587
+ "`storefront` = the storefront shell; `page` = one of your pages (`pageId`)."
11588
+ ).option(
11589
+ "--page <value>",
11590
+ "With `kind: page`: the page that frames the blog (from list-site-template-pages)."
11591
+ ).action(async (kind, options, command) => {
11592
+ const flags = getGlobalFlags(command);
11593
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11594
+ const params = {};
11595
+ const payload = {
11596
+ kind,
11597
+ pageId: options.page
11598
+ };
11599
+ const result = await client.call("PUT", "/site/layout/template", params, void 0, payload);
11600
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11601
+ });
11602
+ const siteMenu = site.command("menu");
11603
+ siteMenu.description("The site's one shared menu (Your site \u2192 Look and menu): up to 8 entries");
11604
+ siteMenu.command("get").summary("Get the shared site menu").description(
11605
+ "The site's shared menu (dashboard: Your site \u2192 Look and menu \u2192 \u201EMenu witryny\u201D) in order: `label`, `style` (`link` / `button`), `target` (`page` + `pageId`, `blog`, `store`, `product` + `productId`, `login`, `url` + `url`) and `href`, the address it leads to today (null = nowhere now; pages leave it out). It fills every page that marks its menu with `data-zanfia-nav` and the blog in its page frame. Empty = every page keeps its own links. Edit it with set-site-menu (the whole list)."
11606
+ ).action(async (_options, command) => {
11607
+ const flags = getGlobalFlags(command);
11608
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11609
+ const params = {};
11610
+ const result = await client.call("GET", "/site/menu", params, void 0, void 0);
11611
+ const rows = tableRowsOf(result, "menu");
11612
+ emit(flags, result ?? {}, () => printTable(rows, getSiteMenuColumns));
11613
+ });
11614
+ siteMenu.command("set").summary("Set the shared site menu").description(
11615
+ 'Replaces the site\'s shared menu with the entries sent, in order (up to 8; an entry left out is removed, an empty list turns the menu off). Live at once on every page that marks its menu with `data-zanfia-nav` and on the blog in its page frame. To add one entry (e.g. the blog: `{ label: "Blog", target: { kind: "blog" }, style: "link" }`) read get-site-menu first and send the whole list. Checked by the dashboard rules: 400 `site-menu-refused` with `issue` (`too-many-entries`, `empty-label`, `label-too-long`, `invalid-url`, `invalid-anchor`, `page-not-found`, `page-not-published`, `product-not-found`) and the entry `index`; nothing saved. Returns the whole layout.'
11616
+ ).requiredOption(
11617
+ "--menu <json|file>",
11618
+ "EVERY entry of the new menu, in order (up to 8), as a JSON array; an empty list turns the shared menu off.",
11619
+ parseJsonArrayFileOrInline("--menu")
11620
+ ).action(async (options, command) => {
11621
+ const flags = getGlobalFlags(command);
11622
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11623
+ const params = {};
11624
+ const payload = {
11625
+ menu: options.menu
11626
+ };
11627
+ const result = await client.call("PUT", "/site/menu", params, void 0, payload);
11628
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11629
+ });
10987
11630
  }
10988
11631
 
10989
11632
  // src/commands/_generated/index.ts
@@ -10992,6 +11635,8 @@ function registerGeneratedCommands(program2) {
10992
11635
  registerBlogCommands(program2);
10993
11636
  registerSiteRedirectsCommands(program2);
10994
11637
  registerPopupsCommands(program2);
11638
+ registerStudioCommands(program2);
11639
+ registerSiteCommands(program2);
10995
11640
  }
10996
11641
 
10997
11642
  // src/commands/ads/index.ts
@@ -11306,18 +11951,18 @@ function buildAdUpdate(options) {
11306
11951
  }
11307
11952
  return input;
11308
11953
  }
11309
- var CONNECTION_ARG = [
11310
- "<connectionId>",
11311
- "Meta connection id (Settings \u2192 Integrations \u2192 Meta)"
11954
+ var CONNECTION_ARG = ["<connectionId>", "Meta connection id (from `ads accounts`)"];
11955
+ var AD_ACCOUNT_ARG = [
11956
+ "<adAccountId>",
11957
+ "Meta ad account id (`act_\u2026`, from `ads accounts`)"
11312
11958
  ];
11313
- var AD_ACCOUNT_ARG = ["<adAccountId>", "Meta ad account id (`act_\u2026`)"];
11314
11959
  var JSON_FLAG_HINT = "(inline JSON or a .json path; JSON output for this command: -o json or the root --json)";
11315
11960
  function registerAdsCommands(program2) {
11316
11961
  const ads = program2.command("ads").description(
11317
11962
  "Meta Ads \u2014 spend vs. Zanfia revenue per ad, and campaign / ad set / ad / creative management on connected ad accounts"
11318
11963
  );
11319
11964
  ads.command("performance").description(
11320
- "Spend per ad (ingested insights) joined with Zanfia orders captured via UTM ad ids, plus a per-day spend / revenue series"
11965
+ "Spend per ad (ingested insights) joined with Zanfia orders captured via UTM ad ids (last click) and with orders of buyers who first came from the ad (first touch), plus a per-day spend / revenue series"
11321
11966
  ).option("--from <date>", "inclusive UTC start, YYYY-MM-DD (server default when omitted)").option("--to <date>", "inclusive UTC end, YYYY-MM-DD (server default when omitted)").option(
11322
11967
  "--checkout <checkoutId>",
11323
11968
  "scope orders and spend to one checkout (spendScope becomes `checkout`)"
@@ -11328,7 +11973,7 @@ function registerAdsCommands(program2) {
11328
11973
  emit(flags, result, () => {
11329
11974
  const currency = result.rows.find((r) => r.currency)?.currency ?? null;
11330
11975
  process.stdout.write(
11331
- `Totals (${result.spendScope} scope): spend ${formatMinor(result.totals.spendMinor, currency)}, ${result.totals.orders} orders, revenue ${formatMinor(result.totals.revenueMinor, currency)}, ROAS ${formatRoas(result.totals.roas) || "n/a"}; ${result.unattributedMetaOrders} Meta orders without an ad id
11976
+ `Totals (${result.spendScope} scope): spend ${formatMinor(result.totals.spendMinor, currency)}, last click: ${result.totals.orders} orders, revenue ${formatMinor(result.totals.revenueMinor, currency)}, ROAS ${formatRoas(result.totals.roas) || "n/a"}; first touch: ${result.totals.firstTouchOrders ?? 0} orders, revenue ${formatMinor(result.totals.firstTouchRevenueMinor ?? 0, currency)}, ROAS ${formatRoas(result.totals.firstTouchRoas ?? null) || "n/a"}; ${result.unattributedMetaOrders} Meta orders without an ad id
11332
11977
 
11333
11978
  `
11334
11979
  );
@@ -11346,7 +11991,10 @@ function registerAdsCommands(program2) {
11346
11991
  metaPurchases: r.metaPurchases,
11347
11992
  orders: r.orders,
11348
11993
  revenue: formatMinor(r.revenueMinor, r.currency),
11349
- roas: formatRoas(r.roas)
11994
+ roas: formatRoas(r.roas),
11995
+ firstTouchOrders: r.firstTouchOrders ?? 0,
11996
+ firstTouchRevenue: formatMinor(r.firstTouchRevenueMinor ?? 0, r.currency),
11997
+ firstTouchRoas: formatRoas(r.firstTouchRoas ?? null)
11350
11998
  })),
11351
11999
  [
11352
12000
  "adId",
@@ -11359,7 +12007,10 @@ function registerAdsCommands(program2) {
11359
12007
  "metaPurchases",
11360
12008
  "orders",
11361
12009
  "revenue",
11362
- "roas"
12010
+ "roas",
12011
+ "firstTouchOrders",
12012
+ "firstTouchRevenue",
12013
+ "firstTouchRoas"
11363
12014
  ]
11364
12015
  );
11365
12016
  process.stdout.write(`
@@ -11376,6 +12027,41 @@ Daily (${result.daily.length})
11376
12027
  );
11377
12028
  });
11378
12029
  });
12030
+ ads.command("accounts").description(
12031
+ "Meta connections and the ad accounts each one reaches (live from Meta: name, currency, status) \u2014 the <connectionId> and <adAccountId> every campaign / ad set / ad command takes"
12032
+ ).action(async (_, command) => {
12033
+ const flags = getGlobalFlags(command);
12034
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12035
+ const result = await client.listMetaAdConnections();
12036
+ emit(flags, result, () => {
12037
+ if (result.connections.length === 0) {
12038
+ process.stdout.write(
12039
+ "No Meta connection \u2014 connect one under Settings \u2192 Integrations \u2192 Ads.\n"
12040
+ );
12041
+ return;
12042
+ }
12043
+ printTable(
12044
+ result.connections.flatMap((connection) => {
12045
+ const head = {
12046
+ connectionId: connection.connectionId,
12047
+ connection: connection.label,
12048
+ issue: connection.issue ?? ""
12049
+ };
12050
+ if (connection.adAccounts.length === 0) {
12051
+ return [{ ...head, adAccountId: "", name: "", currency: "", status: "" }];
12052
+ }
12053
+ return connection.adAccounts.map((account) => ({
12054
+ ...head,
12055
+ adAccountId: account.adAccountId,
12056
+ name: account.name ?? "",
12057
+ currency: account.currency ?? "",
12058
+ status: account.accountStatus ?? ""
12059
+ }));
12060
+ }),
12061
+ ["connectionId", "connection", "adAccountId", "name", "currency", "status", "issue"]
12062
+ );
12063
+ });
12064
+ });
11379
12065
  ads.command("directory").description(
11380
12066
  "Ads known to this workspace (from the insights ingest): ad / ad set / campaign ids and names plus the destination checkout \u2014 the lookup behind `utm_content` ad ids"
11381
12067
  ).action(async (_, command) => {
@@ -11605,6 +12291,27 @@ async function runWhoami(command) {
11605
12291
  const rawApiKey = command.optsWithGlobals().apiKey;
11606
12292
  const logins = listStoredProfiles().length;
11607
12293
  const apiKey = resolveApiKey(flags.apiKey);
12294
+ if (decodeTokenKind(apiKey) === "agent") {
12295
+ const claims = decodeTokenClaims(apiKey);
12296
+ const info2 = {
12297
+ role: "agent",
12298
+ workspaceId: claims.workspaceId ?? "?",
12299
+ agentId: claims.agentId ?? null,
12300
+ uid: claims.subject ?? null,
12301
+ communityApiUrl: resolveCommunityApiUrl()
12302
+ };
12303
+ emit(flags, info2, () => {
12304
+ process.stdout.write(`Role: community agent key (posts and comments go out as the agent)
12305
+ `);
12306
+ process.stdout.write(`Workspace: ${info2.workspaceId}
12307
+ `);
12308
+ process.stdout.write(`Agent: ${info2.agentId ?? "(unknown)"}
12309
+ `);
12310
+ process.stdout.write(`Community API: ${info2.communityApiUrl ?? "<unresolved>"}
12311
+ `);
12312
+ });
12313
+ return;
12314
+ }
11608
12315
  if (decodeTokenKind(apiKey) === "member") {
11609
12316
  const stored = readCredentials();
11610
12317
  const claims = decodeTokenClaims(apiKey);
@@ -11615,7 +12322,8 @@ async function runWhoami(command) {
11615
12322
  workspaceName: stored?.workspaceName ?? null,
11616
12323
  userEmail: stored?.userEmail ?? null,
11617
12324
  uid: claims.subject ?? null,
11618
- scopes: ["communities:read", "communities:write"],
12325
+ scopes: memberKeyScopes(claims),
12326
+ readOnly: isReadOnlyMemberKey(claims),
11619
12327
  communityApiUrl: resolveCommunityApiUrl()
11620
12328
  };
11621
12329
  emit(flags, info2, () => {
@@ -11629,7 +12337,7 @@ async function runWhoami(command) {
11629
12337
  `);
11630
12338
  else if (info2.uid) process.stdout.write(`User id: ${info2.uid}
11631
12339
  `);
11632
- process.stdout.write(`Scopes: ${info2.scopes.join(", ")}
12340
+ process.stdout.write(`Scopes: ${memberScopeLabel(claims)}
11633
12341
  `);
11634
12342
  process.stdout.write(`Community API: ${info2.communityApiUrl ?? "<unresolved>"}
11635
12343
  `);
@@ -11685,10 +12393,15 @@ function registerAuthCommands(program2) {
11685
12393
  process.stdout.write(` status: no credentials file found
11686
12394
  `);
11687
12395
  } else {
12396
+ const storedClaims = decodeTokenClaims(stored.apiKey);
11688
12397
  process.stdout.write(` source: ${stored.source ?? "manual"}
11689
12398
  `);
11690
- process.stdout.write(` kind : ${stored.kind ?? decodeTokenKind(stored.apiKey)}
12399
+ process.stdout.write(` kind : ${stored.kind ?? storedClaims.kind}
12400
+ `);
12401
+ if (storedClaims.kind === "member") {
12402
+ process.stdout.write(` scope : ${memberScopeLabel(storedClaims)}
11691
12403
  `);
12404
+ }
11692
12405
  process.stdout.write(` saved : ${stored.savedAt}
11693
12406
  `);
11694
12407
  if (stored.userEmail) {
@@ -11726,6 +12439,8 @@ Resolved by next call:
11726
12439
  process.stdout.write(` kind : ${kind}
11727
12440
  `);
11728
12441
  if (kind === "member") {
12442
+ process.stdout.write(` scope : ${memberScopeLabel(decodeTokenClaims(apiKey))}
12443
+ `);
11729
12444
  process.stdout.write(
11730
12445
  ` community API: ${resolveCommunityApiUrl() ?? "<unresolved \u2014 set --api-url>"}
11731
12446
  `
@@ -11741,7 +12456,8 @@ Resolved by next call:
11741
12456
  "--api-url <url>",
11742
12457
  "API URL to associate with this key (for member keys, the api-community URL)"
11743
12458
  ).action((key, options) => {
11744
- const kind = decodeTokenKind(key);
12459
+ const claims = decodeTokenClaims(key);
12460
+ const { kind } = claims;
11745
12461
  const path2 = writeCredentials({
11746
12462
  apiKey: key,
11747
12463
  apiUrl: options.apiUrl,
@@ -11749,8 +12465,15 @@ Resolved by next call:
11749
12465
  source: "manual",
11750
12466
  kind
11751
12467
  });
11752
- process.stdout.write(`Saved ${kind === "member" ? "member " : ""}API key to ${path2}
12468
+ const memberLabel = isReadOnlyMemberKey(claims) ? "read-only member " : "member ";
12469
+ const label = kind === "member" ? memberLabel : kind === "agent" ? "agent " : "";
12470
+ process.stdout.write(`Saved ${label}API key to ${path2}
11753
12471
  `);
12472
+ if (kind === "agent") {
12473
+ process.stdout.write(
12474
+ "Note: an agent key only reaches the Community API (`zanfia communities \u2026`); posts and comments go out as the agent.\n"
12475
+ );
12476
+ }
11754
12477
  if (kind === "member" && !options.apiUrl) {
11755
12478
  process.stdout.write(
11756
12479
  "Note: member keys only reach the Community API. Pass --api-url <api-community-url> so `zanfia communities \u2026` knows where to call.\n"
@@ -11771,18 +12494,18 @@ import { platform } from "os";
11771
12494
  async function openBrowser(url) {
11772
12495
  const cmd = browserOpenCommand(url);
11773
12496
  if (!cmd) return false;
11774
- return new Promise((resolve2) => {
12497
+ return new Promise((resolve3) => {
11775
12498
  try {
11776
12499
  const child = spawn(cmd.command, cmd.args, {
11777
12500
  detached: true,
11778
12501
  stdio: "ignore",
11779
12502
  shell: cmd.shell
11780
12503
  });
11781
- child.on("error", () => resolve2(false));
12504
+ child.on("error", () => resolve3(false));
11782
12505
  child.unref();
11783
- setTimeout(() => resolve2(true), 100);
12506
+ setTimeout(() => resolve3(true), 100);
11784
12507
  } catch {
11785
- resolve2(false);
12508
+ resolve3(false);
11786
12509
  }
11787
12510
  });
11788
12511
  }
@@ -11878,11 +12601,11 @@ async function startLoopbackServer() {
11878
12601
  );
11879
12602
  resolveCallback?.({ code, state });
11880
12603
  });
11881
- await new Promise((resolve2, reject) => {
12604
+ await new Promise((resolve3, reject) => {
11882
12605
  server.once("error", reject);
11883
12606
  server.listen(0, "127.0.0.1", () => {
11884
12607
  server.removeListener("error", reject);
11885
- resolve2();
12608
+ resolve3();
11886
12609
  });
11887
12610
  });
11888
12611
  const address = server.address();
@@ -11890,13 +12613,13 @@ async function startLoopbackServer() {
11890
12613
  server.close();
11891
12614
  throw new Error("Could not bind loopback server.");
11892
12615
  }
11893
- const awaitCallback = (expectedState) => new Promise((resolve2, reject) => {
12616
+ const awaitCallback = (expectedState) => new Promise((resolve3, reject) => {
11894
12617
  resolveCallback = (result) => {
11895
12618
  if (result.state !== expectedState) {
11896
12619
  reject(new Error("State mismatch \u2014 possible CSRF. Aborting."));
11897
12620
  return;
11898
12621
  }
11899
- resolve2(result);
12622
+ resolve3(result);
11900
12623
  };
11901
12624
  rejectCallback = reject;
11902
12625
  setTimeout(() => reject(new Error("Login timed out.")), CALLBACK_TIMEOUT_MS).unref();
@@ -12054,6 +12777,17 @@ async function runBrowserAuth(options, entryUrl) {
12054
12777
  server.close();
12055
12778
  }
12056
12779
  }
12780
+ function buildMemberConsentParams(input) {
12781
+ return {
12782
+ redirect_uri: input.redirectUri,
12783
+ state: input.state,
12784
+ code_challenge: input.challenge,
12785
+ code_challenge_method: "S256",
12786
+ hostname: input.hostname,
12787
+ client: "zanfia-cli",
12788
+ ...input.readOnly ? { read_only: "1" } : null
12789
+ };
12790
+ }
12057
12791
  function resolveMemberExchangeUrl() {
12058
12792
  const explicit = process.env.ZANFIA_MEMBER_EXCHANGE_URL ?? backendDefaults.memberExchangeUrl;
12059
12793
  if (explicit) return explicit;
@@ -12098,14 +12832,16 @@ async function runMemberLogin(options) {
12098
12832
  const host = safeHostname();
12099
12833
  const server = await startLoopbackServer();
12100
12834
  const redirectUri = `http://127.0.0.1:${server.port}${CALLBACK_PATH}`;
12101
- const url = buildConsentUrl(consentUrl, {
12102
- redirect_uri: redirectUri,
12103
- state,
12104
- code_challenge: challenge,
12105
- code_challenge_method: "S256",
12106
- hostname: host,
12107
- client: "zanfia-cli"
12108
- });
12835
+ const url = buildConsentUrl(
12836
+ consentUrl,
12837
+ buildMemberConsentParams({
12838
+ redirectUri,
12839
+ state,
12840
+ challenge,
12841
+ hostname: host,
12842
+ readOnly: options.readOnly
12843
+ })
12844
+ );
12109
12845
  process.stdout.write(`Opening ${url}
12110
12846
  `);
12111
12847
  process.stdout.write("Waiting for browser callback...\n\n");
@@ -12131,6 +12867,8 @@ async function runMemberLogin(options) {
12131
12867
  workspaceName: exchanged.workspaceName
12132
12868
  });
12133
12869
  process.stdout.write(`Logged in as ${exchanged.userEmail} (community member).
12870
+ `);
12871
+ process.stdout.write(`Access: ${memberScopeLabel(decodeTokenClaims(exchanged.token))}
12134
12872
  `);
12135
12873
  process.stdout.write(`Workspace: ${exchanged.workspaceName} (${exchanged.workspaceId})
12136
12874
  `);
@@ -12162,6 +12900,9 @@ function registerLoginCommand(auth) {
12162
12900
  ).option(
12163
12901
  "--address <url>",
12164
12902
  "sign in as a community member (Community API) against this creator platform URL"
12903
+ ).option(
12904
+ "--read-only",
12905
+ "member login only: the key can read the communities but never post, comment or chat"
12165
12906
  ).option("-v, --verbose", "show HTTP request details").addHelpText(
12166
12907
  "after",
12167
12908
  `
@@ -12177,24 +12918,33 @@ The two flows use different consent pages:
12177
12918
  command uses another stored login just for that command.
12178
12919
 
12179
12920
  Community member login \u2014 Community API only:
12180
- login --address https://<creator-platform-url>
12921
+ login --address https://<creator-platform-url> [--read-only]
12181
12922
  Opens the consent page on that creator's platform (where you log in as
12182
12923
  their customer). The CLI gets a member token scoped to that workspace's
12183
12924
  communities; the community URL is passed as the platform address, and
12184
- which community to act in is chosen per command.
12925
+ which community to act in is chosen per command. --read-only gets a key
12926
+ that can read and summarize but never post, comment or chat (the right
12927
+ choice for an agent that only reads); \`auth status\` shows the scope.
12185
12928
 
12186
12929
  No browser? Generate a key on the creator's platform under
12187
- Settings -> Developer and paste the shown "auth set-key" command instead.
12930
+ Settings -> AI agents and paste the shown "auth set-key"
12931
+ command instead.
12188
12932
  `
12189
12933
  ).action(async (options) => {
12190
12934
  if (options.address) {
12191
12935
  await runMemberLogin({
12192
12936
  address: options.address,
12193
12937
  apiUrl: options.apiUrl,
12194
- verbose: options.verbose
12938
+ verbose: options.verbose,
12939
+ readOnly: options.readOnly
12195
12940
  });
12196
12941
  return;
12197
12942
  }
12943
+ if (options.readOnly) {
12944
+ throw new Error(
12945
+ "--read-only applies to community member logins: add --address <creator-platform-url> (or use `zanfia join <handle> --read-only`)."
12946
+ );
12947
+ }
12198
12948
  await runLogin(options);
12199
12949
  });
12200
12950
  }
@@ -12231,11 +12981,14 @@ import { hostname as hostname2 } from "os";
12231
12981
  var HttpJsonError = class extends Error {
12232
12982
  status;
12233
12983
  code;
12234
- constructor(message, status, code) {
12984
+ /** The server's own message, without the `<label> failed (<status>):` prefix. */
12985
+ serverMessage;
12986
+ constructor(message, status, code, serverMessage = message) {
12235
12987
  super(message);
12236
12988
  this.name = "HttpJsonError";
12237
12989
  this.status = status;
12238
12990
  this.code = code;
12991
+ this.serverMessage = serverMessage;
12239
12992
  }
12240
12993
  };
12241
12994
  async function postJson2(label, url, body, options = {}) {
@@ -12253,10 +13006,12 @@ async function postJson2(label, url, body, options = {}) {
12253
13006
  });
12254
13007
  const text = await response.text();
12255
13008
  if (!response.ok) {
13009
+ const serverMessage = extractErrorMessage(text, response.statusText);
12256
13010
  throw new HttpJsonError(
12257
- `${label} failed (${response.status}): ${extractErrorMessage(text, response.statusText)}`,
13011
+ `${label} failed (${response.status}): ${serverMessage}`,
12258
13012
  response.status,
12259
- extractErrorCode(text)
13013
+ extractErrorCode(text),
13014
+ serverMessage
12260
13015
  );
12261
13016
  }
12262
13017
  return JSON.parse(text);
@@ -12303,7 +13058,7 @@ async function promptLine(question) {
12303
13058
  function promptHidden(question) {
12304
13059
  const { stdin, stdout } = process;
12305
13060
  if (!stdin.isTTY) return promptLine(question);
12306
- return new Promise((resolve2) => {
13061
+ return new Promise((resolve3) => {
12307
13062
  stdout.write(question);
12308
13063
  stdin.setRawMode(true);
12309
13064
  stdin.resume();
@@ -12315,7 +13070,7 @@ function promptHidden(question) {
12315
13070
  stdin.pause();
12316
13071
  stdin.removeListener("data", onData);
12317
13072
  stdout.write("\n");
12318
- resolve2(value);
13073
+ resolve3(value);
12319
13074
  return;
12320
13075
  }
12321
13076
  if (char === CTRL_C) {
@@ -12677,10 +13432,10 @@ function registerBrandCommands(program2) {
12677
13432
  });
12678
13433
  }
12679
13434
  async function buildUpdatePatch(options) {
12680
- const { readFile: readFile4 } = await import("fs/promises");
13435
+ const { readFile: readFile6 } = await import("fs/promises");
12681
13436
  let patch = {};
12682
13437
  if (options.jsonFile) {
12683
- const raw = await readFile4(options.jsonFile, "utf8");
13438
+ const raw = await readFile6(options.jsonFile, "utf8");
12684
13439
  const parsed = JSON.parse(raw);
12685
13440
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
12686
13441
  throw new Error("--json-file must contain a JSON object with name/visual/editorial keys.");
@@ -12975,6 +13730,12 @@ function printCampaignDetail(campaign) {
12975
13730
  }
12976
13731
  if (campaign.completedAt) process.stdout.write(` Completed at: ${campaign.completedAt}
12977
13732
  `);
13733
+ if (campaign.pacing) {
13734
+ process.stdout.write(
13735
+ ` Pacing: new sending domain ${campaign.pacing.domain}, ~${campaign.pacing.hourlyCap}/h, estimated end ${campaign.pacing.estimatedEndAt}
13736
+ `
13737
+ );
13738
+ }
12978
13739
  const { stats } = campaign;
12979
13740
  process.stdout.write(
12980
13741
  ` Stats: sent ${stats.sent}, delivered ${stats.delivered}, bounced ${stats.bounced}, complained ${stats.complained}, failed ${stats.failed}, skipped ${stats.skippedSuppressed + stats.skippedMuted}
@@ -13105,10 +13866,10 @@ function registerCampaignsCommands(program2) {
13105
13866
  });
13106
13867
  });
13107
13868
  campaigns.command("test").description(
13108
- "Send a TEST of the saved campaign to up to 3 addresses (default: your own) \u2014 subject prefixed [Test], sample personalization values; never the audience"
13869
+ "Send a TEST of the saved campaign to your team's addresses only (default: your own) \u2014 subject prefixed [Test], sample personalization values; never the audience"
13109
13870
  ).argument("<id>", "campaign id").option(
13110
13871
  "--to <email>",
13111
- "test recipient \u2014 repeatable or comma-separated, max 3 (default: your own address)",
13872
+ "test recipient: a team member account e-mail (or a mail-tester.com inbox), repeatable or comma-separated, max 3 (default: your own address)",
13112
13873
  collectRepeatable,
13113
13874
  []
13114
13875
  ).action(async (id, options, command) => {
@@ -14420,7 +15181,13 @@ Next page: --cursor ${page.nextCursor}
14420
15181
  });
14421
15182
  }
14422
15183
  );
14423
- clients2.command("add").description("Add a contact-only client (no product access granted)").requiredOption("--email <email>", "client email").option("--first-name <name>", "client first name").option("--last-name <name>", "client last name").option("--phone <number>", "client phone number").option("--language <code>", "preferred language code (e.g. en, pl)").action(
15184
+ clients2.command("add").description("Add a contact-only client (no product access granted)").requiredOption("--email <email>", "client email").option("--first-name <name>", "client first name").option("--last-name <name>", "client last name").option("--phone <number>", "client phone number").option("--language <code>", "preferred language code (e.g. en, pl)").option(
15185
+ "--marketing-consent",
15186
+ "subscribe the client to marketing e-mail (needs --consent-statement unless the key gives consent)"
15187
+ ).option(
15188
+ "--consent-statement",
15189
+ "your statement: you hold this person's marketing consent and answer for it"
15190
+ ).action(
14424
15191
  async (options, command) => {
14425
15192
  const flags = getGlobalFlags(command);
14426
15193
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -14429,7 +15196,9 @@ Next page: --cursor ${page.nextCursor}
14429
15196
  firstName: options.firstName,
14430
15197
  lastName: options.lastName,
14431
15198
  phoneNumber: options.phone,
14432
- language: options.language
15199
+ language: options.language,
15200
+ ...options.marketingConsent ? { marketingConsent: true } : {},
15201
+ ...options.consentStatement ? { consentStatement: true } : {}
14433
15202
  });
14434
15203
  emit(flags, result, () => {
14435
15204
  process.stdout.write(
@@ -14437,6 +15206,10 @@ Next page: --cursor ${page.nextCursor}
14437
15206
  ` : `Client already exists: ${result.clientId} (${options.email}).
14438
15207
  `
14439
15208
  );
15209
+ if (result.marketingConsent && result.marketingConsent !== "not-requested") {
15210
+ process.stdout.write(`Marketing consent: ${result.marketingConsent}.
15211
+ `);
15212
+ }
14440
15213
  });
14441
15214
  }
14442
15215
  );
@@ -14679,17 +15452,22 @@ Next page: --cursor ${page.nextCursor}
14679
15452
  });
14680
15453
  }
14681
15454
  );
14682
- clients2.command("revoke").description("Revoke a client's product access (NO refund; live subscriptions are refused)").argument("<productId>", "product id").requiredOption("--email <email>", "client email").option("--run-automations", "also run the product's access-end automation tasks").action(
15455
+ clients2.command("revoke").description(
15456
+ "Revoke a client's product access (NO refund). Access whose subscription already ended in Stripe is revoked; a live subscription is refused unless --cancel-subscription"
15457
+ ).argument("<productId>", "product id").requiredOption("--email <email>", "client email").option("--run-automations", "also run the product's access-end automation tasks").option("--cancel-subscription", "cancel a still-live subscription now, then revoke").option("--silent", "no e-mail, automations or workflows for this revoke").action(
14683
15458
  async (productId, options, command) => {
14684
15459
  const flags = getGlobalFlags(command);
14685
15460
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
14686
15461
  const result = await client.revokeClientAccess(productId, {
14687
15462
  email: options.email,
14688
- runAutomations: options.runAutomations
15463
+ runAutomations: options.runAutomations,
15464
+ cancelSubscription: options.cancelSubscription,
15465
+ silent: options.silent
14689
15466
  });
14690
15467
  emit(flags, result, () => {
15468
+ const subscriptionNote = result.subscription === "cancelled" ? " The subscription was cancelled in Stripe." : result.subscription === "already_ended" ? " The subscription had already ended in Stripe." : "";
14691
15469
  process.stdout.write(
14692
- `Revoked ${options.email}'s access to product ${productId} (no refund issued; re-grant with \`clients grant\`).
15470
+ `Revoked ${options.email}'s access to product ${productId} (no refund issued; re-grant with \`clients grant\`).${subscriptionNote}
14693
15471
  `
14694
15472
  );
14695
15473
  });
@@ -14741,7 +15519,8 @@ Next page: --cursor ${page.nextCursor}
14741
15519
  const result = await client.cancelClientSubscription(productId, { email: options.email });
14742
15520
  emit(flags, result, () => {
14743
15521
  process.stdout.write(
14744
- `Cancelled ${options.email}'s subscription to product ${productId} \u2014 access runs to the end of the paid period (check \`clients accesses\` for the date).
15522
+ result.alreadyCancelled ? `${options.email}'s subscription to product ${productId} had already ended in Stripe \u2014 nothing was cancelled now; the access record was brought in line.
15523
+ ` : `Cancelled ${options.email}'s subscription to product ${productId} \u2014 access runs to the end of the paid period (check \`clients accesses\` for the date).
14745
15524
  `
14746
15525
  );
14747
15526
  });
@@ -15146,7 +15925,8 @@ function eventRow(event) {
15146
15925
  endAt: event.endAt,
15147
15926
  timezone: event.timezone,
15148
15927
  status: event.status,
15149
- location: formatEventLocation(event.location)
15928
+ location: formatEventLocation(event.location),
15929
+ rsvp: event.rsvpEnabled === false ? "off" : `${event.goingCount ?? 0} going`
15150
15930
  };
15151
15931
  }
15152
15932
  var EVENT_COLUMNS = [
@@ -15157,8 +15937,19 @@ var EVENT_COLUMNS = [
15157
15937
  "endAt",
15158
15938
  "timezone",
15159
15939
  "status",
15160
- "location"
15940
+ "location",
15941
+ "rsvp"
15161
15942
  ];
15943
+ function attendeeRow(attendee) {
15944
+ return {
15945
+ memberId: attendee.memberId,
15946
+ name: [attendee.firstName, attendee.lastName].filter(Boolean).join(" "),
15947
+ email: attendee.email ?? "",
15948
+ answeredAt: attendee.respondedAt,
15949
+ access: attendee.hasAccess ? "yes" : "lost"
15950
+ };
15951
+ }
15952
+ var ATTENDEE_COLUMNS = ["memberId", "name", "email", "answeredAt", "access"];
15162
15953
  function moderationRow(report) {
15163
15954
  const reason = report.reporters.map((r) => r.reason).find((r) => Boolean(r)) ?? "";
15164
15955
  return {
@@ -15814,7 +16605,10 @@ ${message.content}
15814
16605
  );
15815
16606
  communities2.command("members").description(
15816
16607
  "List community members (staff only). Filters --role, --disabled and --email-prefix are mutually exclusive."
15817
- ).argument("<communityId>", "community id").option("--role <roleId>", "only members holding this role").option("--disabled <true|false>", "only disabled (soft-banned) / only active members").option("--email-prefix <prefix>", "email prefix search (case-insensitive)").option("--limit <n>", "page size (max 100)").option("--cursor <cursor>", "pagination cursor from a previous page").action(
16608
+ ).argument("<communityId>", "community id").option("--role <roleId>", "only members holding this role").option("--disabled <true|false>", "only disabled (soft-banned) / only active members").option(
16609
+ "--email-prefix <prefix>",
16610
+ "email prefix search (case-insensitive; workspace keys only, a member key gets 403)"
16611
+ ).option("--limit <n>", "page size (max 100)").option("--cursor <cursor>", "pagination cursor from a previous page").action(
15818
16612
  async (communityId, options, command) => {
15819
16613
  const flags = getGlobalFlags(command);
15820
16614
  const filterCount = [options.role, options.disabled, options.emailPrefix].filter(
@@ -15922,8 +16716,11 @@ Next page: --cursor ${result.nextCursor}
15922
16716
  });
15923
16717
  });
15924
16718
  settings.command("update").description(
15925
- "Patch community settings \u2014 via toggle flags and/or a JSON patch. Allowed patch keys: uploadVideo, membersDisplay, viewStyle, displayName, icon, hiddenWhenUnavailable, voiceMessages, enableMemberApiAccess, publicAccess, publicAccessTermsUrl, contentVisibleFromJoinDate, eventsEnabled, directMessagesEnabled, gamification, moderation, assistant, digest. Nested blocks in the JSON patch replace their whole sub-object; the --gamification / --auto-hide-after-reports flags merge into the existing block instead."
15926
- ).argument("<communityId>", "community id").option("--patch <fileOrJson>", "the settings patch as inline JSON or a path to a .json file").option("--events-enabled <on|off>", "show / hide the Calendar (events) section for members").option("--direct-messages <on|off>", "allow / block member \u2194 member direct messages").option(
16719
+ "Patch community settings \u2014 via toggle flags and/or a JSON patch. Allowed patch keys: uploadVideo, membersDisplay, viewStyle, displayName, icon, hiddenWhenUnavailable, voiceMessages, enableMemberApiAccess, publicAccess, publicAccessTermsUrl, contentVisibleFromJoinDate, eventsEnabled, eventAnnouncements, directMessagesEnabled, gamification, moderation, assistant, digest. Nested blocks in the JSON patch replace their whole sub-object; the --gamification / --auto-hide-after-reports flags merge into the existing block instead."
16720
+ ).argument("<communityId>", "community id").option("--patch <fileOrJson>", "the settings patch as inline JSON or a path to a .json file").option("--events-enabled <on|off>", "show / hide the Calendar (events) section for members").option(
16721
+ "--event-announcements <on|off>",
16722
+ "bell + e-mail to members when a new event is published (default on)"
16723
+ ).option("--direct-messages <on|off>", "allow / block member \u2194 member direct messages").option(
15927
16724
  "--content-visible-from-join-date <on|off>",
15928
16725
  '"archive from purchase date" \u2014 members only see posts newer than their own access start'
15929
16726
  ).option("--gamification <on|off>", "XP / levels / leaderboards (stored reputation is kept)").option(
@@ -15937,6 +16734,12 @@ Next page: --cursor ${result.nextCursor}
15937
16734
  if (options.eventsEnabled !== void 0) {
15938
16735
  patch.eventsEnabled = parseOnOff(options.eventsEnabled, "--events-enabled");
15939
16736
  }
16737
+ if (options.eventAnnouncements !== void 0) {
16738
+ patch.eventAnnouncements = parseOnOff(
16739
+ options.eventAnnouncements,
16740
+ "--event-announcements"
16741
+ );
16742
+ }
15940
16743
  if (options.directMessages !== void 0) {
15941
16744
  patch.directMessagesEnabled = parseOnOff(options.directMessages, "--direct-messages");
15942
16745
  }
@@ -15964,7 +16767,7 @@ Next page: --cursor ${result.nextCursor}
15964
16767
  const keys = Object.keys(patch);
15965
16768
  if (keys.length === 0) {
15966
16769
  throw new Error(
15967
- "Nothing to update \u2014 pass --patch and/or one of the toggle flags (--events-enabled, --direct-messages, --content-visible-from-join-date, --gamification, --auto-hide-after-reports)."
16770
+ "Nothing to update \u2014 pass --patch and/or one of the toggle flags (--events-enabled, --event-announcements, --direct-messages, --content-visible-from-join-date, --gamification, --auto-hide-after-reports)."
15968
16771
  );
15969
16772
  }
15970
16773
  const result = await client.updateCommunitySettings(communityId, patch);
@@ -16105,9 +16908,12 @@ Next page: --cursor ${result.nextCursor}
16105
16908
  );
16106
16909
  events.command("create").description(
16107
16910
  'Create a calendar event (staff). Flags override --json; `--location` is {"type":"online","url":\u2026} or {"type":"inPerson","address":\u2026}.'
16108
- ).argument("<communityId>", "community id").option("--title <title>", "event title").option("--description <text>", "plain-text description").option("--start <iso>", "start \u2014 ISO 8601 instant (e.g. 2026-09-01T17:00:00Z)").option("--end <iso>", "end \u2014 ISO 8601 instant").option("--timezone <iana>", "IANA timezone the event is authored in (e.g. Europe/Warsaw)").option("--type <kind>", `event kind: ${COMMUNITY_EVENT_KINDS.join("|")} (default other)`).option("--all-day", "all-day event (start/end span whole days, no times shown)").option("--location <fileOrJson>", "location object as inline JSON or a .json file path").option(
16911
+ ).argument("<communityId>", "community id").option("--title <title>", "event title").option("--description <text>", "plain-text description").option("--start <iso>", "start \u2014 ISO 8601 instant (e.g. 2026-09-01T17:00:00Z)").option("--end <iso>", "end \u2014 ISO 8601 instant").option("--timezone <iana>", "IANA timezone the event is authored in (e.g. Europe/Warsaw)").option("--type <kind>", `event kind: ${COMMUNITY_EVENT_KINDS.join("|")} (default other)`).option("--all-day", "all-day event (start/end span whole days, no times shown)").option("--location <fileOrJson>", "location object as inline JSON or a .json file path").option("--rsvp <on|off>", "\u201EZbieraj zapisy\u201D: members answer going / not going (default on)").option(
16912
+ "--announce <on|off>",
16913
+ "\u201EPowiadom cz\u0142onk\xF3w\u201D: bell + one e-mail \u201ENowe wydarzenie\u201D to members (default on)"
16914
+ ).option(
16109
16915
  "--json <file>",
16110
- "full request body from a .json file (title, description, startAt, endAt, timezone, type, allDay, location, coverImage)"
16916
+ "full request body from a .json file (title, description, startAt, endAt, timezone, type, allDay, location, coverImage, rsvpEnabled, announce)"
16111
16917
  ).action(
16112
16918
  async (communityId, options, command) => {
16113
16919
  const flags = getGlobalFlags(command);
@@ -16121,7 +16927,9 @@ Next page: --cursor ${result.nextCursor}
16121
16927
  ...options.timezone !== void 0 ? { timezone: options.timezone } : {},
16122
16928
  ...options.type !== void 0 ? { type: assertOneOf(options.type, COMMUNITY_EVENT_KINDS, "--type") } : {},
16123
16929
  ...options.allDay ? { allDay: true } : {},
16124
- ...options.location !== void 0 ? { location: parseEventLocation(options.location) } : {}
16930
+ ...options.location !== void 0 ? { location: parseEventLocation(options.location) } : {},
16931
+ ...options.rsvp !== void 0 ? { rsvpEnabled: parseOnOff(options.rsvp, "--rsvp") } : {},
16932
+ ...options.announce !== void 0 ? { announce: parseOnOff(options.announce, "--announce") } : {}
16125
16933
  };
16126
16934
  const missing = ["title", "startAt", "endAt", "timezone", "location"].filter(
16127
16935
  (key) => input[key] === void 0
@@ -16146,7 +16954,7 @@ Next page: --cursor ${result.nextCursor}
16146
16954
  );
16147
16955
  events.command("update").description(
16148
16956
  "Update a calendar event (staff). Every flag is optional; `--status cancelled` cancels without deleting, `--status scheduled` restores."
16149
- ).argument("<communityId>", "community id").argument("<eventId>", "event id").option("--title <title>", "new title").option("--description <text>", "new plain-text description").option("--start <iso>", "new start \u2014 ISO 8601 instant").option("--end <iso>", "new end \u2014 ISO 8601 instant").option("--timezone <iana>", "new IANA timezone").option("--type <kind>", `event kind: ${COMMUNITY_EVENT_KINDS.join("|")}`).option("--all-day <on|off>", "switch the all-day mode on/off").option("--location <fileOrJson>", "new location object as inline JSON or a .json file path").option("--status <status>", `${SETTABLE_COMMUNITY_EVENT_STATUSES.join("|")}`).option("--json <file>", "patch body from a .json file (flags override)").action(
16957
+ ).argument("<communityId>", "community id").argument("<eventId>", "event id").option("--title <title>", "new title").option("--description <text>", "new plain-text description").option("--start <iso>", "new start \u2014 ISO 8601 instant").option("--end <iso>", "new end \u2014 ISO 8601 instant").option("--timezone <iana>", "new IANA timezone").option("--type <kind>", `event kind: ${COMMUNITY_EVENT_KINDS.join("|")}`).option("--all-day <on|off>", "switch the all-day mode on/off").option("--location <fileOrJson>", "new location object as inline JSON or a .json file path").option("--status <status>", `${SETTABLE_COMMUNITY_EVENT_STATUSES.join("|")}`).option("--rsvp <on|off>", "\u201EZbieraj zapisy\u201D: collect going / not going answers or stop").option("--json <file>", "patch body from a .json file (flags override)").action(
16150
16958
  async (communityId, eventId, options, command) => {
16151
16959
  const flags = getGlobalFlags(command);
16152
16960
  const base = options.json !== void 0 ? parseJsonObject(options.json, "--json") : {};
@@ -16166,7 +16974,8 @@ Next page: --cursor ${result.nextCursor}
16166
16974
  SETTABLE_COMMUNITY_EVENT_STATUSES,
16167
16975
  "--status"
16168
16976
  )
16169
- } : {}
16977
+ } : {},
16978
+ ...options.rsvp !== void 0 ? { rsvpEnabled: parseOnOff(options.rsvp, "--rsvp") } : {}
16170
16979
  };
16171
16980
  if (Object.keys(input).length === 0) {
16172
16981
  throw new Error("Nothing to update \u2014 pass at least one flag (or a --json patch).");
@@ -16192,6 +17001,31 @@ Next page: --cursor ${result.nextCursor}
16192
17001
  });
16193
17002
  }
16194
17003
  );
17004
+ events.command("attendees").description("Who answered \u201EB\u0119d\u0119\u201D (going) / \u201ENie b\u0119d\u0119\u201D (not going), with e-mails (staff only)").argument("<communityId>", "community id").argument("<eventId>", "event id (from `communities events list`)").action(async (communityId, eventId, _, command) => {
17005
+ const flags = getGlobalFlags(command);
17006
+ const result = await clientFor(flags).listCommunityEventAttendees(communityId, eventId);
17007
+ emit(flags, result, () => {
17008
+ const going = result.attendees.filter(
17009
+ (attendee) => attendee.status === "going"
17010
+ );
17011
+ const notGoing = result.attendees.filter(
17012
+ (attendee) => attendee.status === "not_going"
17013
+ );
17014
+ if (!result.rsvpEnabled) {
17015
+ process.stdout.write("RSVP is off for this event (answers given before are kept).\n");
17016
+ }
17017
+ process.stdout.write(`Going (${result.goingCount}):
17018
+ `);
17019
+ printTable(going.map(attendeeRow), ATTENDEE_COLUMNS);
17020
+ process.stdout.write(`
17021
+ Not going (${result.notGoingCount}):
17022
+ `);
17023
+ printTable(notGoing.map(attendeeRow), ATTENDEE_COLUMNS);
17024
+ if (result.truncated) {
17025
+ process.stdout.write("\nThe list stops at 1000 answers; the counts are exact.\n");
17026
+ }
17027
+ });
17028
+ });
16195
17029
  const moderation = communities2.command("moderation").description("Reported posts and comments \u2014 review the queue and resolve reports (staff)");
16196
17030
  moderation.command("queue").description("Show pending reports plus the most recently resolved ones").argument("<communityId>", "community id").action(async (communityId, _, command) => {
16197
17031
  const flags = getGlobalFlags(command);
@@ -16245,6 +17079,7 @@ var SETTINGS_PATCH_KEYS = [
16245
17079
  "publicAccessTermsUrl",
16246
17080
  "contentVisibleFromJoinDate",
16247
17081
  "eventsEnabled",
17082
+ "eventAnnouncements",
16248
17083
  "directMessagesEnabled",
16249
17084
  "gamification",
16250
17085
  "moderation",
@@ -17503,7 +18338,7 @@ function registerDiscountsCommands(program2) {
17503
18338
  page.items.map((d) => ({
17504
18339
  id: d.id,
17505
18340
  code: d.code,
17506
- value: formatDiscountValue(d.percentOff, d.amountOffCents, d.currency),
18341
+ value: formatDiscountValue(d.percentOff, d.amountOffCents, d.currency, d.amountScope),
17507
18342
  active: d.active ? "yes" : "no",
17508
18343
  uses: d.usageCount
17509
18344
  })),
@@ -17516,7 +18351,10 @@ function registerDiscountsCommands(program2) {
17516
18351
  "limit to a product (repeat the flag for several)",
17517
18352
  collectRepeatable4,
17518
18353
  []
17519
- ).option("--max-redemptions <n>", "maximum number of redemptions").option("--ends <isoDate>", "date the code stops working (ISO, e.g. 2026-12-31)").option("--starts <isoDate>", "date the code starts working (ISO, e.g. 2026-12-01)").option("--forever", "apply to every renewal of a subscription (default: first payment only)").option("--inactive", "create the code deactivated (default: active)").action(
18354
+ ).option("--max-redemptions <n>", "maximum number of redemptions").option("--ends <isoDate>", "date the code stops working (ISO, e.g. 2026-12-31)").option("--starts <isoDate>", "date the code starts working (ISO, e.g. 2026-12-01)").option("--forever", "apply to every renewal of a subscription (default: first payment only)").option(
18355
+ "--per-unit",
18356
+ "with --amount-off: take the amount off each unit bought (default: once per order)"
18357
+ ).option("--inactive", "create the code deactivated (default: active)").action(
17520
18358
  async (options, command) => {
17521
18359
  const flags = getGlobalFlags(command);
17522
18360
  if (options.percentOff === void 0 === (options.amountOff === void 0)) {
@@ -17528,12 +18366,15 @@ function registerDiscountsCommands(program2) {
17528
18366
  if (options.currency !== void 0 && options.amountOff === void 0) {
17529
18367
  throw new Error("--currency only applies with --amount-off.");
17530
18368
  }
18369
+ if (options.perUnit && options.amountOff === void 0) {
18370
+ throw new Error("--per-unit only applies with --amount-off.");
18371
+ }
17531
18372
  const percentOff = options.percentOff !== void 0 ? parsePercent(options.percentOff, "--percent-off") : void 0;
17532
18373
  const amountOffCents = options.amountOff !== void 0 ? parsePositiveInt8(options.amountOff, "--amount-off") : void 0;
17533
18374
  const maxRedemptions = options.maxRedemptions !== void 0 ? parsePositiveInt8(options.maxRedemptions, "--max-redemptions") : void 0;
17534
18375
  const endsAt = options.ends !== void 0 ? parseIsoDate(options.ends, "--ends") : void 0;
17535
18376
  const startsAt = options.starts !== void 0 ? parseIsoDate(options.starts, "--starts") : void 0;
17536
- const duration = options.forever ? "forever" : void 0;
18377
+ const duration2 = options.forever ? "forever" : void 0;
17537
18378
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
17538
18379
  const result = await client.createDiscount({
17539
18380
  code: options.code,
@@ -17544,14 +18385,16 @@ function registerDiscountsCommands(program2) {
17544
18385
  maxRedemptions,
17545
18386
  endsAt,
17546
18387
  startsAt,
17547
- duration,
18388
+ duration: duration2,
18389
+ amountScope: options.perUnit ? "unit" : void 0,
17548
18390
  active: options.inactive ? false : void 0
17549
18391
  });
17550
18392
  emit(flags, result, () => {
17551
18393
  const value = formatDiscountValue(
17552
18394
  result.percentOff,
17553
18395
  result.amountOffCents,
17554
- result.currency
18396
+ result.currency,
18397
+ result.amountScope
17555
18398
  );
17556
18399
  const inactiveNote = result.active ? "" : " [inactive]";
17557
18400
  process.stdout.write(
@@ -17566,7 +18409,10 @@ function registerDiscountsCommands(program2) {
17566
18409
  "replace the product restriction (repeat the flag for several)",
17567
18410
  collectRepeatable4,
17568
18411
  []
17569
- ).option("--max-redemptions <n>", "maximum number of redemptions").option("--clear-max-redemptions", "remove the redemption limit").option("--ends <isoDate>", "date the code stops working (ISO, e.g. 2026-12-31)").option("--clear-ends", "remove the end date").option("--forever", "apply to every renewal of a subscription").option("--active", "activate the code").option("--inactive", "deactivate the code").action(
18412
+ ).option("--max-redemptions <n>", "maximum number of redemptions").option("--clear-max-redemptions", "remove the redemption limit").option("--ends <isoDate>", "date the code stops working (ISO, e.g. 2026-12-31)").option("--clear-ends", "remove the end date").option("--forever", "apply to every renewal of a subscription").option(
18413
+ "--amount-scope <scope>",
18414
+ "fixed amounts: 'order' (once per order) or 'unit' (off each unit bought)"
18415
+ ).option("--active", "activate the code").option("--inactive", "deactivate the code").action(
17570
18416
  async (discountId, options, command) => {
17571
18417
  const flags = getGlobalFlags(command);
17572
18418
  if (options.percentOff !== void 0 && options.amountOff !== void 0) {
@@ -17597,6 +18443,9 @@ function registerDiscountsCommands(program2) {
17597
18443
  if (options.ends !== void 0) input.endsAt = parseIsoDate(options.ends, "--ends");
17598
18444
  if (options.clearEnds) input.endsAt = null;
17599
18445
  if (options.forever) input.duration = "forever";
18446
+ if (options.amountScope !== void 0) {
18447
+ input.amountScope = parseAmountScope(options.amountScope);
18448
+ }
17600
18449
  if (options.active) input.active = true;
17601
18450
  if (options.inactive) input.active = false;
17602
18451
  if (Object.keys(input).length === 0) {
@@ -17610,7 +18459,8 @@ function registerDiscountsCommands(program2) {
17610
18459
  const value = formatDiscountValue(
17611
18460
  result.percentOff,
17612
18461
  result.amountOffCents,
17613
- result.currency
18462
+ result.currency,
18463
+ result.amountScope
17614
18464
  );
17615
18465
  const state = result.active ? "active" : "inactive";
17616
18466
  process.stdout.write(
@@ -17643,12 +18493,18 @@ function registerDiscountsCommands(program2) {
17643
18493
  });
17644
18494
  });
17645
18495
  }
17646
- function formatDiscountValue(percentOff, amountOffCents, currency) {
18496
+ function formatDiscountValue(percentOff, amountOffCents, currency, amountScope) {
17647
18497
  if (percentOff !== void 0) return `${percentOff}%`;
17648
- if (amountOffCents !== void 0)
17649
- return `${(amountOffCents / 100).toFixed(2)} ${currency ?? ""}`.trim();
18498
+ if (amountOffCents !== void 0) {
18499
+ const amount = `${(amountOffCents / 100).toFixed(2)} ${currency ?? ""}`.trim();
18500
+ return amountScope === "unit" ? `${amount} per unit` : amount;
18501
+ }
17650
18502
  return "";
17651
18503
  }
18504
+ function parseAmountScope(value) {
18505
+ if (value === "order" || value === "unit") return value;
18506
+ throw new Error("--amount-scope must be 'order' or 'unit'.");
18507
+ }
17652
18508
 
17653
18509
  // src/commands/emails/index.ts
17654
18510
  import { readFileSync as readFileSync12 } from "fs";
@@ -18110,20 +18966,357 @@ function registerEmailsCommands(program2) {
18110
18966
  printBranding(result);
18111
18967
  });
18112
18968
  });
18113
- emails.command("test <key>").description(
18114
- "Queue a test send of one system email (saved customization, sample data) to YOUR own address"
18115
- ).option("--lang <en|pl>", "language to render (defaults to 'pl')").action(async (key, options, command) => {
18969
+ emails.command("test <key>").description(
18970
+ "Queue a test send of one system email (saved customization, sample data) to YOUR own address"
18971
+ ).option("--lang <en|pl>", "language to render (defaults to 'pl')").action(async (key, options, command) => {
18972
+ const flags = getGlobalFlags(command);
18973
+ const language = parseLanguage(options.lang, "pl");
18974
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18975
+ const result = await client.sendEmailTemplateTest(key, language);
18976
+ emit(flags, result, () => {
18977
+ process.stdout.write(`Test queued \u2192 ${result.email}
18978
+ `);
18979
+ });
18980
+ });
18981
+ registerEmailLibraryCommands(emails);
18982
+ registerEmailSenderCommands(emails);
18983
+ }
18984
+
18985
+ // src/commands/files/index.ts
18986
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
18987
+ import { resolve } from "path";
18988
+
18989
+ // src/commands/files/localFileSet.ts
18990
+ import { createHash as createHash3 } from "crypto";
18991
+ import { readdir, readFile, stat } from "fs/promises";
18992
+ import { join as join2, relative, sep } from "path";
18993
+ var MAX_FILE_BYTES = 15 * 1024 * 1024;
18994
+ var MAX_OPERATIONS_PER_REQUEST = 400;
18995
+ var ALWAYS_IGNORED = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", "Thumbs.db"]);
18996
+ var ZANFIA_IGNORE_FILE = ".zanfiaignore";
18997
+ var IGNORE_FILES = [".gitignore", ZANFIA_IGNORE_FILE];
18998
+ var escapeRegex = (text) => text.replace(/[.+^${}()|\\]/g, "\\$&");
18999
+ var globToRegex = (glob) => {
19000
+ let out = "";
19001
+ for (let index = 0; index < glob.length; index += 1) {
19002
+ const char = glob[index];
19003
+ if (char === "*") {
19004
+ if (glob[index + 1] === "*") {
19005
+ const slashAfter = glob[index + 2] === "/";
19006
+ out += slashAfter ? "(?:.*/)?" : ".*";
19007
+ index += slashAfter ? 2 : 1;
19008
+ } else {
19009
+ out += "[^/]*";
19010
+ }
19011
+ } else if (char === "?") {
19012
+ out += "[^/]";
19013
+ } else if (char === "[") {
19014
+ const end = glob.indexOf("]", index + 1);
19015
+ if (end === -1) {
19016
+ out += "\\[";
19017
+ } else {
19018
+ const body = glob.slice(index + 1, end);
19019
+ out += `[${body.startsWith("!") ? `^${body.slice(1)}` : body}]`;
19020
+ index = end;
19021
+ }
19022
+ } else {
19023
+ out += escapeRegex(char ?? "");
19024
+ }
19025
+ }
19026
+ return out;
19027
+ };
19028
+ var parseIgnoreRules = (text) => text.split(/\r?\n/).flatMap((rawLine) => {
19029
+ let line = rawLine.replace(/\s+$/, "");
19030
+ if (!line || line.startsWith("#")) return [];
19031
+ const negated = line.startsWith("!");
19032
+ if (negated) line = line.slice(1);
19033
+ if (line.startsWith("\\")) line = line.slice(1);
19034
+ const dirOnly = line.endsWith("/");
19035
+ if (dirOnly) line = line.slice(0, -1);
19036
+ if (!line) return [];
19037
+ const anchored = line.includes("/");
19038
+ if (line.startsWith("/")) line = line.slice(1);
19039
+ const body = globToRegex(line);
19040
+ return [
19041
+ {
19042
+ regex: new RegExp(anchored ? `^${body}$` : `^(?:.*/)?${body}$`),
19043
+ negated,
19044
+ dirOnly
19045
+ }
19046
+ ];
19047
+ });
19048
+ var isIgnoredByRules = (path2, isDir, rules) => {
19049
+ let ignored = false;
19050
+ for (const rule of rules) {
19051
+ if (rule.dirOnly && !isDir) continue;
19052
+ if (rule.regex.test(path2)) ignored = !rule.negated;
19053
+ }
19054
+ return ignored;
19055
+ };
19056
+ var TEMPLATE_SUFFIXES = ["example", "sample", "template", "dist", "defaults"];
19057
+ var CREDENTIALS_FILE_NAMES = [
19058
+ ".netrc",
19059
+ "_netrc",
19060
+ ".git-credentials",
19061
+ ".pypirc",
19062
+ ".pgpass",
19063
+ "secrets.json",
19064
+ "secrets.yml",
19065
+ "secrets.yaml"
19066
+ ];
19067
+ var secretFileReason = (path2) => {
19068
+ const name = (path2.split("/").pop() ?? path2).toLowerCase();
19069
+ const segments = name.split(".");
19070
+ const first = segments[0] ?? "";
19071
+ const last = segments[segments.length - 1] ?? "";
19072
+ if (segments.length > 1 && (TEMPLATE_SUFFIXES.includes(last) || TEMPLATE_SUFFIXES.includes(first))) {
19073
+ return null;
19074
+ }
19075
+ if (name === ".env" || name === ".envrc" || /^\.env[._-]/.test(name) || name.endsWith(".env")) {
19076
+ return "env-file";
19077
+ }
19078
+ if (["id_rsa", "id_dsa", "id_ecdsa", "id_ed25519"].includes(name)) return "ssh-key";
19079
+ if (CREDENTIALS_FILE_NAMES.includes(name)) return "credentials-file";
19080
+ if (name === "credentials" && path2.toLowerCase().split("/").includes(".aws")) {
19081
+ return "credentials-file";
19082
+ }
19083
+ const extension = segments.length > 1 ? last : "";
19084
+ if (["pem", "key", "p12", "pfx", "jks", "keystore", "ppk", "kdbx"].includes(extension)) {
19085
+ return "key-file";
19086
+ }
19087
+ return null;
19088
+ };
19089
+ var readIgnoreRules = async (root) => {
19090
+ const rules = [];
19091
+ for (const name of IGNORE_FILES) {
19092
+ try {
19093
+ rules.push(...parseIgnoreRules(await readFile(join2(root, name), "utf8")));
19094
+ } catch {
19095
+ }
19096
+ }
19097
+ return rules;
19098
+ };
19099
+ var scanLocalFileSet = async (root) => {
19100
+ const rules = await readIgnoreRules(root);
19101
+ const scan = { files: [], ignored: 0, refused: [] };
19102
+ const walk = async (directory) => {
19103
+ const entries = await readdir(directory, { withFileTypes: true });
19104
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
19105
+ const absolutePath = join2(directory, entry.name);
19106
+ const path2 = relative(root, absolutePath).split(sep).join("/").normalize("NFC");
19107
+ const isDir = entry.isDirectory();
19108
+ if (ALWAYS_IGNORED.has(entry.name) || // `.zanfiaignore` lists what the creator keeps private: it never ships.
19109
+ path2 === ZANFIA_IGNORE_FILE || isIgnoredByRules(path2, isDir, rules)) {
19110
+ scan.ignored += 1;
19111
+ continue;
19112
+ }
19113
+ if (isDir) {
19114
+ await walk(absolutePath);
19115
+ continue;
19116
+ }
19117
+ if (!entry.isFile()) continue;
19118
+ const secret = secretFileReason(path2);
19119
+ if (secret) {
19120
+ scan.refused.push({ path: path2, reason: `secret (${secret})` });
19121
+ continue;
19122
+ }
19123
+ const { size } = await stat(absolutePath);
19124
+ if (size > MAX_FILE_BYTES) {
19125
+ scan.refused.push({ path: path2, reason: "larger than 15 MB" });
19126
+ continue;
19127
+ }
19128
+ const sha256 = createHash3("sha256").update(await readFile(absolutePath)).digest("hex");
19129
+ scan.files.push({ path: path2, size, sha256, absolutePath });
19130
+ }
19131
+ };
19132
+ await walk(root);
19133
+ return scan;
19134
+ };
19135
+ var planPush = (local, remote, options) => {
19136
+ const prefix = options.targetFolder ? `${options.targetFolder}/` : "";
19137
+ const mapped = local.map((file) => ({ ...file, path: `${prefix}${file.path}` }));
19138
+ const remoteByPath = new Map(remote.map((file) => [file.path, file.sha256]));
19139
+ const localPaths = new Set(mapped.map((file) => file.path));
19140
+ const plan = { added: [], updated: [], unchanged: [], missing: [] };
19141
+ for (const file of mapped) {
19142
+ const remoteSha = remoteByPath.get(file.path);
19143
+ if (remoteSha === void 0) plan.added.push(file);
19144
+ else if (remoteSha === file.sha256) plan.unchanged.push(file.path);
19145
+ else plan.updated.push(file);
19146
+ }
19147
+ plan.missing = remote.map((file) => file.path).filter((path2) => path2.startsWith(prefix) && !localPaths.has(path2));
19148
+ return plan;
19149
+ };
19150
+ var chunkPaths = (paths, size = MAX_OPERATIONS_PER_REQUEST) => {
19151
+ const chunks = [];
19152
+ for (let index = 0; index < paths.length; index += size) {
19153
+ chunks.push(paths.slice(index, index + size));
19154
+ }
19155
+ return chunks;
19156
+ };
19157
+ var PUSH_BATCH_MAX_BYTES = 12 * 1024 * 1024;
19158
+ var PUSH_BATCH_MAX_FILES = 200;
19159
+ var batchForPush = (files) => {
19160
+ const batches = [];
19161
+ let current = [];
19162
+ let bytes = 0;
19163
+ for (const file of files) {
19164
+ if (current.length > 0 && (bytes + file.size > PUSH_BATCH_MAX_BYTES || current.length >= PUSH_BATCH_MAX_FILES)) {
19165
+ batches.push(current);
19166
+ current = [];
19167
+ bytes = 0;
19168
+ }
19169
+ current.push(file);
19170
+ bytes += file.size;
19171
+ }
19172
+ if (current.length > 0) batches.push(current);
19173
+ return batches;
19174
+ };
19175
+
19176
+ // src/commands/files/index.ts
19177
+ var parseTargetFolder = (value) => {
19178
+ if (!value) return "";
19179
+ const folder = value.normalize("NFC").replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
19180
+ if (folder.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
19181
+ throw new Error(`--to "${value}" is not a folder path inside the set.`);
19182
+ }
19183
+ return folder;
19184
+ };
19185
+ var formatBytes = (bytes) => {
19186
+ if (bytes < 1024) return `${bytes} B`;
19187
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
19188
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
19189
+ };
19190
+ var filesOffNotice = (answer) => answer.fileSetEnabled === false && answer.fileCount > 0 ? "Files are off for buyers (\u201EPliki dla kupuj\u0105cych\u201D is off): turn it on in the product's Settings.\n" : null;
19191
+ var writeFilesOffNotice = (answer) => {
19192
+ const notice = filesOffNotice(answer);
19193
+ if (notice) process.stderr.write(notice);
19194
+ };
19195
+ var requireProduct = (options) => {
19196
+ if (!options.product) throw new Error("Provide the product via --product <id>.");
19197
+ return options.product;
19198
+ };
19199
+ function registerFilesCommands(program2) {
19200
+ const files = program2.command("files").description("A product's \u201EPliki\u201D: the folder tree its buyers browse and download");
19201
+ files.command("list").description("List the files of a product (path, size, sha256)").requiredOption("--product <id>", "product id").action(async (options, command) => {
19202
+ const flags = getGlobalFlags(command);
19203
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19204
+ const fileSet = await client.getProductFileSet(requireProduct(options));
19205
+ emit(flags, fileSet, () => {
19206
+ process.stdout.write(
19207
+ `${fileSet.fileCount} files, ${formatBytes(fileSet.totalBytes)}, revision ${fileSet.revision}${fileSet.lastMessage ? ` (${fileSet.lastMessage})` : ""}
19208
+ `
19209
+ );
19210
+ writeFilesOffNotice(fileSet);
19211
+ printTable(
19212
+ fileSet.files.map((file) => ({
19213
+ path: file.path,
19214
+ size: formatBytes(file.size),
19215
+ sha256: file.sha256.slice(0, 12)
19216
+ })),
19217
+ ["path", "size", "sha256"]
19218
+ );
19219
+ });
19220
+ });
19221
+ files.command("push").description(
19222
+ "Push a folder into the product's \u201EPliki\u201D: uploads only changed files (by sha256), honours the folder's root .gitignore / .zanfiaignore, refuses secrets. Files of the set that are not in the folder are KEPT unless you pass --delete"
19223
+ ).argument("<dir>", "folder to push").requiredOption("--product <id>", "product id").option("--to <folder>", "folder of the set to push into (default: the set's root)").option("-m, --message <text>", "what changed (kept as the set's latest note)").option("--dry-run", "print the plan, send nothing").option(
19224
+ "--delete",
19225
+ "also delete set files (inside --to) that are not in the folder: a mirror push"
19226
+ ).action(async (dir, options, command) => {
18116
19227
  const flags = getGlobalFlags(command);
18117
- const language = parseLanguage(options.lang, "pl");
18118
- const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18119
- const result = await client.sendEmailTemplateTest(key, language);
18120
- emit(flags, result, () => {
18121
- process.stdout.write(`Test queued \u2192 ${result.email}
19228
+ const productId = requireProduct(options);
19229
+ const targetFolder = parseTargetFolder(options.to);
19230
+ const root = resolve(dir);
19231
+ if (!(await stat2(root)).isDirectory()) throw new Error(`${dir} is not a folder.`);
19232
+ const scan = await scanLocalFileSet(root);
19233
+ if (scan.refused.length > 0) {
19234
+ throw new Error(
19235
+ `Nothing pushed. Remove or ignore these files first:
19236
+ ${scan.refused.map((item) => ` ${item.path}: ${item.reason}`).join("\n")}`
19237
+ );
19238
+ }
19239
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19240
+ const remote = await client.getProductFileSet(productId);
19241
+ const plan = planPush(scan.files, remote.files, { targetFolder });
19242
+ const uploads = [...plan.added, ...plan.updated];
19243
+ const deletes = options.delete ? plan.missing : [];
19244
+ const summary = {
19245
+ added: plan.added.map((file) => file.path),
19246
+ updated: plan.updated.map((file) => file.path),
19247
+ deleted: deletes,
19248
+ kept: options.delete ? [] : plan.missing,
19249
+ unchanged: plan.unchanged.length,
19250
+ ignored: scan.ignored
19251
+ };
19252
+ const printPlan = () => {
19253
+ for (const path2 of summary.added) process.stdout.write(` + ${path2}
19254
+ `);
19255
+ for (const path2 of summary.updated) process.stdout.write(` ~ ${path2}
19256
+ `);
19257
+ for (const path2 of summary.deleted) process.stdout.write(` - ${path2}
19258
+ `);
19259
+ process.stdout.write(
19260
+ `${summary.added.length} new, ${summary.updated.length} changed, ${summary.deleted.length} deleted, ${summary.unchanged} unchanged, ${summary.ignored} ignored
19261
+ `
19262
+ );
19263
+ if (summary.kept.length > 0) {
19264
+ process.stdout.write(
19265
+ `${summary.kept.length} file(s) of the set are not in this folder and were kept (pass --delete to remove them).
19266
+ `
19267
+ );
19268
+ }
19269
+ };
19270
+ if (options.dryRun || uploads.length === 0 && deletes.length === 0) {
19271
+ emit(flags, { ...summary, dryRun: Boolean(options.dryRun) }, () => {
19272
+ printPlan();
19273
+ if (!options.dryRun) process.stdout.write("Nothing to push: the set is up to date.\n");
19274
+ });
19275
+ return;
19276
+ }
19277
+ const requests = [
19278
+ ...batchForPush(uploads).map((batch) => async () => ({
19279
+ files: await Promise.all(
19280
+ batch.map(async (file) => ({
19281
+ path: file.path,
19282
+ contentBase64: (await readFile2(file.absolutePath)).toString("base64")
19283
+ }))
19284
+ )
19285
+ })),
19286
+ ...chunkPaths(deletes).map((deletePaths) => async () => ({ deletePaths }))
19287
+ ];
19288
+ let result = null;
19289
+ for (const [index, build] of requests.entries()) {
19290
+ const isLast = index === requests.length - 1;
19291
+ const input = await build();
19292
+ try {
19293
+ result = await client.changeProductFileSet(productId, {
19294
+ ...input,
19295
+ ...isLast && options.message ? { message: options.message } : {}
19296
+ });
19297
+ } catch (error) {
19298
+ if (index > 0) {
19299
+ process.stderr.write(
19300
+ `Stopped at request ${index + 1}/${requests.length}: the ${index} before it were saved, nothing after it was sent. Fix the files below and push again (unchanged files are skipped).
19301
+ `
19302
+ );
19303
+ }
19304
+ throw error;
19305
+ }
19306
+ if (!flags.json && requests.length > 1) {
19307
+ process.stderr.write(` sent ${index + 1}/${requests.length}
18122
19308
  `);
19309
+ }
19310
+ }
19311
+ emit(flags, { ...summary, revision: result?.revision ?? remote.revision }, () => {
19312
+ printPlan();
19313
+ process.stdout.write(
19314
+ `Pushed: revision ${result?.revision ?? remote.revision}, ${result?.fileCount ?? 0} files, ${formatBytes(result?.totalBytes ?? 0)}.
19315
+ `
19316
+ );
19317
+ if (result) writeFilesOffNotice(result);
18123
19318
  });
18124
19319
  });
18125
- registerEmailLibraryCommands(emails);
18126
- registerEmailSenderCommands(emails);
18127
19320
  }
18128
19321
 
18129
19322
  // src/commands/forms/index.ts
@@ -18490,7 +19683,13 @@ function registerFormsCommands(program2) {
18490
19683
 
18491
19684
  // src/commands/funnels/index.ts
18492
19685
  import { readFileSync as readFileSync14 } from "fs";
18493
- var CREATABLE_RECIPES = ["lead_magnet", "tripwire", "sell_product"];
19686
+ var CREATABLE_RECIPES = [
19687
+ "lead_magnet",
19688
+ "tripwire",
19689
+ "low_ticket",
19690
+ "sell_product",
19691
+ "webinar"
19692
+ ];
18494
19693
  var FUNNEL_STATUSES = ["draft", "live", "archived"];
18495
19694
  var FUNNEL_LANGS = ["en", "pl"];
18496
19695
  var FUNNEL_TEMPLATE_IDS = [
@@ -18529,6 +19728,13 @@ function withLiveGateHint(error, funnelId) {
18529
19728
  }
18530
19729
  throw error;
18531
19730
  }
19731
+ function parseMeetingLink(value, flag) {
19732
+ const trimmed = value.trim();
19733
+ if (!/^https?:\/\/\S+$/i.test(trimmed)) {
19734
+ throw new Error(`Invalid ${flag}: ${value} (expected an http(s) link)`);
19735
+ }
19736
+ return trimmed;
19737
+ }
18532
19738
  function parsePositiveInt10(value, flag) {
18533
19739
  const n = Number.parseInt(value, 10);
18534
19740
  if (!Number.isInteger(n) || n <= 0) {
@@ -18630,6 +19836,33 @@ function printFunnelDetail(funnel) {
18630
19836
  ["#", "stepId", "type", "status", "name", "slug"]
18631
19837
  );
18632
19838
  }
19839
+ function printFunnelEvent(event) {
19840
+ const start = event.startAt ? `${event.startAt} (${event.timezone}, ${event.durationMinutes} min)` : "on demand";
19841
+ process.stdout.write(` Webinar (page ${event.pageId}):
19842
+ `);
19843
+ process.stdout.write(` Start: ${start}
19844
+ `);
19845
+ process.stdout.write(` Join link: ${event.joinUrl ?? "(none)"}
19846
+ `);
19847
+ if (event.replayUrl) process.stdout.write(` Recording: ${event.replayUrl}
19848
+ `);
19849
+ if (event.liveRoomId) process.stdout.write(` Runs on webinar: ${event.liveRoomId}
19850
+ `);
19851
+ if (event.rungs.length === 0) {
19852
+ process.stdout.write(" Reminders: none\n");
19853
+ return;
19854
+ }
19855
+ process.stdout.write(" Reminders:\n");
19856
+ printTable(
19857
+ event.rungs.map((rung) => ({
19858
+ kind: rung.kind,
19859
+ sendAt: rung.sendAt,
19860
+ status: rung.status ?? "missing",
19861
+ broadcastId: rung.broadcastId
19862
+ })),
19863
+ ["kind", "sendAt", "status", "broadcastId"]
19864
+ );
19865
+ }
18633
19866
  async function resolveStepId(client, funnelId, step) {
18634
19867
  if (!/^\d+$/.test(step)) return step;
18635
19868
  const position = Number.parseInt(step, 10);
@@ -18655,7 +19888,7 @@ function buildFunnelUpdateInput(options) {
18655
19888
  }
18656
19889
  function registerFunnelsCommands(program2) {
18657
19890
  const funnels = program2.command("funnels").description(
18658
- "Manage marketing funnels \u2014 named step chains that REFERENCE existing checkouts, pages, and workflows (a funnel never owns the artifacts its steps point to). Create is recipe-first: lead_magnet, tripwire, sell_product (webinar is a coming-soon recipe, not creatable yet)."
19891
+ `Manage marketing funnels \u2014 named step chains that REFERENCE existing checkouts, pages, and workflows (a funnel never owns the artifacts its steps point to). Create is recipe-first (${CREATABLE_RECIPES.join(", ")}) or from a template with its pages generated in one go (\`funnels from-template\`).`
18659
19892
  );
18660
19893
  funnels.command("list").description("List the workspace's funnels").action(async (_, command) => {
18661
19894
  const flags = getGlobalFlags(command);
@@ -18677,10 +19910,13 @@ function registerFunnelsCommands(program2) {
18677
19910
  const flags = getGlobalFlags(command);
18678
19911
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18679
19912
  const result = await client.getFunnel(id);
18680
- emit(flags, result, () => printFunnelDetail(result.funnel));
19913
+ emit(flags, result, () => {
19914
+ printFunnelDetail(result.funnel);
19915
+ if (result.event) printFunnelEvent(result.event);
19916
+ });
18681
19917
  });
18682
19918
  funnels.command("create").description(
18683
- "Create a funnel from a recipe over the product's EXISTING artifacts. 'lead_magnet' (free checkout as opt-in \u2192 thank-you) and 'tripwire' (free opt-in \u2192 the checkout's one-time-offer chain \u2192 thank-you) REQUIRE a free checkout on the product; 'sell_product' (sales page \u2192 default paid checkout \u2192 OTO \u2192 thank-you) requires a paid one. Creates the funnel DRAFT referencing those existing checkouts (no new pages or offers) plus the recipe email sequence as a DRAFT workflow \u2014 nothing sends until the creator publishes it."
19919
+ "Create a funnel from a recipe over the product's EXISTING artifacts. 'lead_magnet' (free checkout as opt-in \u2192 thank-you) and 'tripwire' (free opt-in \u2192 the checkout's one-time-offer chain \u2192 thank-you) REQUIRE a free checkout on the product; 'sell_product' (sales page \u2192 default paid checkout \u2192 OTO \u2192 thank-you) and 'low_ticket' (a cheap product page that takes the payment \u2192 one-time offers \u2192 thank-you) require a paid one. Creates the funnel DRAFT referencing those existing checkouts (no new pages or offers) plus the recipe email sequence as a DRAFT workflow \u2014 nothing sends until the creator publishes it. 'webinar' takes any product and creates three EMPTY page steps (registration \u2192 thank-you \u2192 watch page); generate the registration step next with the event (`funnels generate-page <id> --step 1 --event-start \u2026`), or build it all in one go with `funnels from-template webinar`."
18684
19920
  ).requiredOption("--recipe <recipe>", `creation recipe (${CREATABLE_RECIPES.join(" | ")})`).requiredOption("--product <productId>", "the product the funnel sells or delivers").option("--name <name>", "custom name; omit for a recipe default").requiredOption("--lang <en|pl>", "language of the generated email copy \u2014 the creator's").action(async (options, command) => {
18685
19921
  const flags = getGlobalFlags(command);
18686
19922
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -18694,6 +19930,14 @@ function registerFunnelsCommands(program2) {
18694
19930
  emit(flags, result, () => {
18695
19931
  process.stdout.write(`Created funnel ${result.funnelId} (draft).
18696
19932
  `);
19933
+ if (input.recipe === "webinar") {
19934
+ process.stdout.write(
19935
+ `Next: generate the registration page with the event (it builds the thank-you and watch pages and the reminder drafts too):
19936
+ funnels generate-page ${result.funnelId} --step 1 --lang ${input.lang} --event-start <ISO 8601 with offset> --event-timezone <IANA zone>
19937
+ (on-demand: --event-mode on_demand instead of the date)
19938
+ `
19939
+ );
19940
+ }
18697
19941
  });
18698
19942
  });
18699
19943
  funnels.command("update").description(
@@ -18740,11 +19984,23 @@ function registerFunnelsCommands(program2) {
18740
19984
  });
18741
19985
  });
18742
19986
  funnels.command("generate-page").description(
18743
- "AI-generate a hosted DRAFT page for ONE page-type step (opt_in_page / sales_page / thank_you \u2014 checkout/oto/downsell steps have their own surfaces) and wire the step to it. Consumes AI credits and runs 1-3 minutes synchronously \u2014 call it once per step, never speculatively. Pass the same --template across a funnel\u2019s steps so the pages look like siblings."
19987
+ "AI-generate a hosted DRAFT page for ONE page-type step (lead-magnet / sales-page / thank-you / webinar-registration \u2014 checkout/oto/downsell steps have their own surfaces) and wire the step to it. A webinar-registration step builds the whole family (thank-you, watch page, reminder drafts) around the event: pass --event-start (+ --event-timezone, --event-duration) or --event-mode on_demand. Consumes AI credits and runs in the background (1-5 minutes) \u2014 call it once per step, never speculatively. Pass the same --template across a funnel\u2019s steps so the pages look like siblings."
18744
19988
  ).argument("<id>", "funnel id").requiredOption(
18745
19989
  "--step <n|id>",
18746
19990
  "the page-type step to build \u2014 either its 1-based position (from `funnels get`'s step table) or its raw stepId"
18747
- ).requiredOption("--lang <en|pl>", "language of the generated copy \u2014 the creator's").option("--brief <text>", "optional creator notes appended to the synthesized brief").option("--template <templateId>", "optional design-template seed; reuse across steps").action(
19991
+ ).requiredOption("--lang <en|pl>", "language of the generated copy \u2014 the creator's").option("--brief <text>", "optional creator notes appended to the synthesized brief").option("--template <templateId>", "optional design-template seed; reuse across steps").option(
19992
+ "--event-start <iso>",
19993
+ "webinar registration step: event start as ISO 8601 WITH offset (live)"
19994
+ ).option("--event-timezone <tz>", "webinar registration step: IANA timezone, e.g. Europe/Warsaw").option(
19995
+ "--event-duration <minutes>",
19996
+ "webinar registration step: event length in minutes (default 60)"
19997
+ ).option(
19998
+ "--event-mode <mode>",
19999
+ `webinar registration step: ${EVENT_MODES.join(" | ")} (on_demand = no date)`
20000
+ ).option(
20001
+ "--join-url <url>",
20002
+ "webinar registration step: the meeting link (ClickMeeting, Zoom, Teams, Meet)"
20003
+ ).action(
18748
20004
  async (id, options, command) => {
18749
20005
  const flags = getGlobalFlags(command);
18750
20006
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -18753,7 +20009,12 @@ function registerFunnelsCommands(program2) {
18753
20009
  stepId,
18754
20010
  lang: parseChoice3(options.lang, FUNNEL_LANGS, "--lang"),
18755
20011
  brief: options.brief,
18756
- templateId: options.template
20012
+ templateId: options.template,
20013
+ ...options.eventStart !== void 0 ? { eventStartAt: options.eventStart } : {},
20014
+ ...options.eventTimezone !== void 0 ? { eventTimezone: options.eventTimezone } : {},
20015
+ ...options.eventDuration !== void 0 ? { eventDurationMinutes: parsePositiveInt10(options.eventDuration, "--event-duration") } : {},
20016
+ ...options.eventMode !== void 0 ? { eventMode: parseChoice3(options.eventMode, EVENT_MODES, "--event-mode") } : {},
20017
+ ...options.joinUrl !== void 0 ? { eventJoinUrl: parseMeetingLink(options.joinUrl, "--join-url") } : {}
18757
20018
  };
18758
20019
  const result = await client.generateFunnelStepPage(id, input);
18759
20020
  emit(flags, result, () => {
@@ -18785,22 +20046,48 @@ function registerFunnelsCommands(program2) {
18785
20046
  });
18786
20047
  });
18787
20048
  funnels.command("from-template").description(
18788
- "Create a funnel from a catalog template (`funnels templates`) AND AI-generate its page steps in one go \u2014 opt-in / sales / thank-you pages are queued as background generations (202; consumes AI credits, 1-5 minutes each). The template\u2019s product requirement must be met (free or paid checkout on the product), else 409. Webinar template: pass the event fields (live) or --event-mode on_demand. Requires --confirm."
18789
- ).argument("<templateId>", `template id (${FUNNEL_TEMPLATE_IDS.join(" | ")})`).requiredOption("--product <productId>", "the product the funnel sells or delivers").requiredOption("--lang <en|pl>", "language of the generated pages and emails \u2014 the creator's").option("--name <name>", "custom funnel name; omit for the recipe default").option("--event-start <iso>", "webinar: event start as ISO 8601 WITH offset (live)").option("--event-timezone <tz>", "webinar: IANA timezone, e.g. Europe/Warsaw").option("--event-duration <minutes>", "webinar: event length in minutes (default 60)").option("--event-mode <mode>", `webinar: ${EVENT_MODES.join(" | ")} (on_demand = autowebinar)`).option(
20049
+ "Create a funnel from a catalog template (`funnels templates`) AND AI-generate its page steps in one go \u2014 opt-in / sales / thank-you pages are queued as background generations (202; consumes AI credits, 1-5 minutes each). The template\u2019s product requirement must be met (free or paid checkout on the product), else 409. Webinar template: pass the event fields (live; --join-url for a ClickMeeting / Zoom link) or --event-mode on_demand, or --live-room <roomId> to run it on a webinar that plays a recording (then --product is optional; needs the live_rooms plan feature). Requires --confirm."
20050
+ ).argument("<templateId>", `template id (${FUNNEL_TEMPLATE_IDS.join(" | ")})`).option(
20051
+ "--product <productId>",
20052
+ "the product the funnel sells or delivers (required, except with --live-room)"
20053
+ ).requiredOption("--lang <en|pl>", "language of the generated pages and emails \u2014 the creator's").option("--name <name>", "custom funnel name; omit for the recipe default").option("--event-start <iso>", "webinar: event start as ISO 8601 WITH offset (live)").option("--event-timezone <tz>", "webinar: IANA timezone, e.g. Europe/Warsaw").option("--event-duration <minutes>", "webinar: event length in minutes (default 60)").option(
20054
+ "--event-mode <mode>",
20055
+ `webinar: ${EVENT_MODES.join(" | ")} (on_demand = on-demand webinar, no date)`
20056
+ ).option(
20057
+ "--join-url <url>",
20058
+ "webinar (live): the meeting link (ClickMeeting, Zoom, Teams, Meet) the pages and reminders carry"
20059
+ ).option(
20060
+ "--live-room <roomId>",
20061
+ "webinar: a webinar (`live-rooms list`) that plays a recording; its sessions are the event"
20062
+ ).option(
18790
20063
  "--page-template <id>",
18791
20064
  "design template for every generated page (a page-template id, e.g. course-fresh); thank-you steps take the companion"
18792
20065
  ).option("--confirm", "required \u2014 acknowledge that AI credits will be consumed").action(
18793
20066
  async (templateId, options, command) => {
18794
20067
  const flags = getGlobalFlags(command);
20068
+ const template = parseChoice3(templateId, FUNNEL_TEMPLATE_IDS, "<templateId>");
20069
+ if (options.liveRoom !== void 0 && template !== "webinar") {
20070
+ throw new Error("--live-room is for the webinar template only.");
20071
+ }
20072
+ if (options.joinUrl !== void 0 && template !== "webinar") {
20073
+ throw new Error("--join-url is for the webinar template only.");
20074
+ }
20075
+ if (options.product === void 0 && options.liveRoom === void 0) {
20076
+ throw new Error(
20077
+ "Missing --product <productId> (only a webinar that plays a recording, --live-room, goes without one)."
20078
+ );
20079
+ }
18795
20080
  requireConfirm6(options.confirm, "create the funnel and generate its pages (AI credits)");
18796
20081
  const input = {
18797
- templateId: parseChoice3(templateId, FUNNEL_TEMPLATE_IDS, "<templateId>"),
18798
- productId: options.product,
20082
+ templateId: template,
20083
+ ...options.product !== void 0 ? { productId: options.product } : {},
18799
20084
  lang: parseChoice3(options.lang, FUNNEL_LANGS, "--lang"),
18800
20085
  name: options.name,
18801
20086
  ...options.pageTemplate ? { pageTemplateId: options.pageTemplate } : {}
18802
20087
  };
18803
20088
  const webinar = {
20089
+ liveRoomId: options.liveRoom,
20090
+ joinUrl: options.joinUrl !== void 0 ? parseMeetingLink(options.joinUrl, "--join-url") : void 0,
18804
20091
  eventStartAt: options.eventStart,
18805
20092
  eventTimezone: options.eventTimezone,
18806
20093
  eventDurationMinutes: options.eventDuration !== void 0 ? parsePositiveInt10(options.eventDuration, "--event-duration") : void 0,
@@ -19843,6 +21130,16 @@ function registerIntegrationsCommands(program2) {
19843
21130
  }
19844
21131
 
19845
21132
  // src/commands/join/index.ts
21133
+ function joinApprovalRefusal(err) {
21134
+ if (!(err instanceof HttpJsonError)) return null;
21135
+ if (err.code === "community-join-approval-required" /* ApprovalRequired */) {
21136
+ return err.serverMessage.endsWith(".") ? err.serverMessage : `${err.serverMessage}.`;
21137
+ }
21138
+ if (err.code === "community-join-approval-unavailable" /* ApprovalUnavailable */) {
21139
+ return "We couldn't check your sign-up right now, please try again in a moment.";
21140
+ }
21141
+ return null;
21142
+ }
19846
21143
  var MAX_CODE_ATTEMPTS2 = 5;
19847
21144
  var HANDLE_REGEX = /^[a-z0-9][a-z0-9-]{1,62}$/;
19848
21145
  var EMAIL_REGEX2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -19900,10 +21197,18 @@ async function runJoin(handleInput, options) {
19900
21197
  completed = await postJson2(
19901
21198
  "Join",
19902
21199
  joinUrl(communityApiUrl, handle, "complete"),
19903
- { email, code, firstName, hostname: hostname3 },
21200
+ {
21201
+ email,
21202
+ code,
21203
+ firstName,
21204
+ hostname: hostname3,
21205
+ ...options.readOnly ? { readOnly: true } : {}
21206
+ },
19904
21207
  { verbose }
19905
21208
  );
19906
21209
  } catch (err) {
21210
+ const refusal = joinApprovalRefusal(err);
21211
+ if (refusal) throw new Error(refusal, { cause: err });
19907
21212
  if (options.code || attempt === MAX_CODE_ATTEMPTS2) throw err;
19908
21213
  process.stdout.write(`${err instanceof Error ? err.message : String(err)}
19909
21214
  `);
@@ -19923,11 +21228,14 @@ async function runJoin(handleInput, options) {
19923
21228
  workspaceName: completed.workspaceName,
19924
21229
  kind: "member"
19925
21230
  });
21231
+ const claims = decodeTokenClaims(completed.token);
21232
+ const readOnly = isReadOnlyMemberKey(claims);
19926
21233
  if (options.json) {
19927
21234
  process.stdout.write(
19928
21235
  `${JSON.stringify(
19929
21236
  {
19930
21237
  joined: true,
21238
+ readOnly,
19931
21239
  alreadyMember: completed.alreadyMember,
19932
21240
  communityId: completed.communityId,
19933
21241
  communityName: completed.communityName,
@@ -19945,12 +21253,13 @@ async function runJoin(handleInput, options) {
19945
21253
  );
19946
21254
  return;
19947
21255
  }
19948
- const intro = completed.introChannelId ? ` zanfia communities post create ${completed.communityId} ${completed.introChannelId} --content "Hi, I'm <your agent>. I ..."
21256
+ const intro = completed.introChannelId && !readOnly ? ` zanfia communities post create ${completed.communityId} ${completed.introChannelId} --content "Hi, I'm <your agent>. I ..."
19949
21257
  ` : "";
19950
21258
  process.stdout.write(
19951
21259
  `
19952
21260
  ${completed.alreadyMember ? "Welcome back to" : "You are in:"} ${completed.communityName}.
19953
21261
  Member key saved to ${path2} (expires ${completed.expiresAt.slice(0, 10)}).
21262
+ Access: ${memberScopeLabel(claims)}
19954
21263
 
19955
21264
  Next:
19956
21265
  zanfia communities list
@@ -19963,17 +21272,20 @@ Plug in your agent: ${AGENT_DOCS_URL}
19963
21272
  );
19964
21273
  }
19965
21274
  function registerJoinCommand(program2) {
19966
- program2.command("join <handle>").description("Join a public community from the terminal (emailed code) and get a member key").option("--email <email>", "email to join with (prompted when omitted)").option("--name <name>", "your display name (prompted when omitted)").option("--code <code>", "the 6-digit code from the email (prompted when omitted)").addHelpText(
21275
+ program2.command("join <handle>").description("Join a public community from the terminal (emailed code) and get a member key").option("--email <email>", "email to join with (prompted when omitted)").option("--name <name>", "your display name (prompted when omitted)").option("--code <code>", "the 6-digit code from the email (prompted when omitted)").option("--read-only", "get a key that can read and summarize but never post, comment or chat").addHelpText(
19967
21276
  "after",
19968
21277
  `
19969
21278
  Examples:
19970
21279
  zanfia join crew-of-one
19971
21280
  zanfia join crew-of-one --email me@example.com --name "Sam" --code 123456
21281
+ zanfia join crew-of-one --read-only
19972
21282
 
19973
21283
  The key is a MEMBER key: it reaches the Community API for that creator's
19974
- communities only (read/post/comment/chat), never your own workspace. It is
19975
- saved to ~/.config/zanfia/credentials.json and expires in 90 days \u2014 run
19976
- join again to refresh it.
21284
+ communities only (read/post/comment/chat), never your own workspace. With
21285
+ --read-only it can only read (the right choice for an agent that reads and
21286
+ summarizes); \`zanfia auth status\` shows the scope. It is saved to
21287
+ ~/.config/zanfia/credentials.json and expires in 90 days \u2014 run join again
21288
+ to refresh it.
19977
21289
  `
19978
21290
  ).action(async (handle, options, command) => {
19979
21291
  const globals = getGlobalFlags(command);
@@ -20009,9 +21321,9 @@ function describeSchedule(room) {
20009
21321
  }
20010
21322
  function registerLiveRoomsCommands(program2) {
20011
21323
  const liveRooms = program2.command("live-rooms").description(
20012
- "Live rooms (webinar rooms, incl. rolling autowebinar sessions) \u2014 list and attendance; rooms are managed in the dashboard"
21324
+ "Webinars (incl. rolling autowebinar sessions) \u2014 list and attendance; webinars are managed in the dashboard"
20013
21325
  );
20014
- liveRooms.command("list").description("List live rooms (latest start first)").action(async (_, command) => {
21326
+ liveRooms.command("list").description("List webinars (latest start first)").action(async (_, command) => {
20015
21327
  const flags = getGlobalFlags(command);
20016
21328
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
20017
21329
  const result = await client.listLiveRooms();
@@ -20030,7 +21342,7 @@ function registerLiveRoomsCommands(program2) {
20030
21342
  );
20031
21343
  });
20032
21344
  });
20033
- liveRooms.command("attendance").description("A room's attendance per session; --session adds that session's viewers").argument("<roomId>", "live room id (from live-rooms list)").option("--session <iso>", "ISO start of one session to list its attendees").action(async (roomId, options, command) => {
21345
+ liveRooms.command("attendance").description("A webinar's attendance per session; --session adds that session's viewers").argument("<roomId>", "webinar id (from live-rooms list)").option("--session <iso>", "ISO start of one session to list its attendees").action(async (roomId, options, command) => {
20034
21346
  const flags = getGlobalFlags(command);
20035
21347
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
20036
21348
  const result = await client.getLiveRoomAttendance(roomId, options.session);
@@ -20066,7 +21378,7 @@ function registerLiveRoomsCommands(program2) {
20066
21378
  if (totals) {
20067
21379
  process.stdout.write(
20068
21380
  `
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)" : ""}
21381
+ Webinar 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
21382
  `
20071
21383
  );
20072
21384
  }
@@ -20093,12 +21405,12 @@ Viewers of ${options.session}:
20093
21405
 
20094
21406
  // src/commands/media/index.ts
20095
21407
  import { existsSync, readFileSync as readFileSync17, writeFileSync as writeFileSync4 } from "fs";
20096
- import { readFile, stat as stat3 } from "fs/promises";
21408
+ import { readFile as readFile3, stat as stat5 } from "fs/promises";
20097
21409
  import { basename } from "path";
20098
21410
 
20099
21411
  // src/client/bunnyUpload.ts
20100
21412
  import { createReadStream as createReadStream2 } from "fs";
20101
- import { stat as stat2 } from "fs/promises";
21413
+ import { stat as stat4 } from "fs/promises";
20102
21414
 
20103
21415
  // ../../node_modules/tus-js-client/lib.esm/error.js
20104
21416
  function _typeof(o) {
@@ -20744,7 +22056,7 @@ function _regeneratorRuntime() {
20744
22056
  return this.delegate = { iterator: values(e2), resultName: r2, nextLoc: n2 }, "next" === this.method && (this.arg = t), y;
20745
22057
  } }, e;
20746
22058
  }
20747
- function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
22059
+ function asyncGeneratorStep(gen, resolve3, reject, _next, _throw, key, arg) {
20748
22060
  try {
20749
22061
  var info = gen[key](arg);
20750
22062
  var value = info.value;
@@ -20753,7 +22065,7 @@ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
20753
22065
  return;
20754
22066
  }
20755
22067
  if (info.done) {
20756
- resolve2(value);
22068
+ resolve3(value);
20757
22069
  } else {
20758
22070
  Promise.resolve(value).then(_next, _throw);
20759
22071
  }
@@ -20761,13 +22073,13 @@ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
20761
22073
  function _asyncToGenerator(fn) {
20762
22074
  return function() {
20763
22075
  var self2 = this, args = arguments;
20764
- return new Promise(function(resolve2, reject) {
22076
+ return new Promise(function(resolve3, reject) {
20765
22077
  var gen = fn.apply(self2, args);
20766
22078
  function _next(value) {
20767
- asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
22079
+ asyncGeneratorStep(gen, resolve3, reject, _next, _throw, "next", value);
20768
22080
  }
20769
22081
  function _throw(err) {
20770
- asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
22082
+ asyncGeneratorStep(gen, resolve3, reject, _next, _throw, "throw", err);
20771
22083
  }
20772
22084
  _next(void 0);
20773
22085
  });
@@ -21099,7 +22411,7 @@ var BaseUpload = /* @__PURE__ */ (function() {
21099
22411
  var lastPartProgress = 0;
21100
22412
  return _this3._source.slice(part.start, part.end).then(function(_ref) {
21101
22413
  var value = _ref.value;
21102
- return new Promise(function(resolve2, reject) {
22414
+ return new Promise(function(resolve3, reject) {
21103
22415
  var options = _objectSpread(_objectSpread({}, _this3.options), {}, {
21104
22416
  // If available, the partial upload should be resumed from a previous URL.
21105
22417
  uploadUrl: part.uploadUrl || null,
@@ -21117,7 +22429,7 @@ var BaseUpload = /* @__PURE__ */ (function() {
21117
22429
  "Upload-Concat": "partial"
21118
22430
  }),
21119
22431
  // Reject or resolve the promise if the upload errors or completes.
21120
- onSuccess: resolve2,
22432
+ onSuccess: resolve3,
21121
22433
  onError: reject,
21122
22434
  // Based in the progress for this partial upload, calculate the progress
21123
22435
  // for the entire final upload.
@@ -21628,8 +22940,8 @@ var BaseUpload = /* @__PURE__ */ (function() {
21628
22940
  var newOptions = _objectSpread(_objectSpread({}, options), {}, {
21629
22941
  retryDelays: remainingDelays
21630
22942
  });
21631
- return new Promise(function(resolve2) {
21632
- return setTimeout(resolve2, delay);
22943
+ return new Promise(function(resolve3) {
22944
+ return setTimeout(resolve3, delay);
21633
22945
  }).then(function() {
21634
22946
  return BaseUpload2.terminate(url, newOptions);
21635
22947
  });
@@ -22093,7 +23405,7 @@ function _toPrimitive5(t, r) {
22093
23405
  }
22094
23406
  return ("string" === r ? String : Number)(t);
22095
23407
  }
22096
- function asyncGeneratorStep2(gen, resolve2, reject, _next, _throw, key, arg) {
23408
+ function asyncGeneratorStep2(gen, resolve3, reject, _next, _throw, key, arg) {
22097
23409
  try {
22098
23410
  var info = gen[key](arg);
22099
23411
  var value = info.value;
@@ -22102,7 +23414,7 @@ function asyncGeneratorStep2(gen, resolve2, reject, _next, _throw, key, arg) {
22102
23414
  return;
22103
23415
  }
22104
23416
  if (info.done) {
22105
- resolve2(value);
23417
+ resolve3(value);
22106
23418
  } else {
22107
23419
  Promise.resolve(value).then(_next, _throw);
22108
23420
  }
@@ -22110,13 +23422,13 @@ function asyncGeneratorStep2(gen, resolve2, reject, _next, _throw, key, arg) {
22110
23422
  function _asyncToGenerator2(fn) {
22111
23423
  return function() {
22112
23424
  var self2 = this, args = arguments;
22113
- return new Promise(function(resolve2, reject) {
23425
+ return new Promise(function(resolve3, reject) {
22114
23426
  var gen = fn.apply(self2, args);
22115
23427
  function _next(value) {
22116
- asyncGeneratorStep2(gen, resolve2, reject, _next, _throw, "next", value);
23428
+ asyncGeneratorStep2(gen, resolve3, reject, _next, _throw, "next", value);
22117
23429
  }
22118
23430
  function _throw(err) {
22119
- asyncGeneratorStep2(gen, resolve2, reject, _next, _throw, "throw", err);
23431
+ asyncGeneratorStep2(gen, resolve3, reject, _next, _throw, "throw", err);
22120
23432
  }
22121
23433
  _next(void 0);
22122
23434
  });
@@ -22468,7 +23780,7 @@ function _toPrimitive6(t, r) {
22468
23780
  }
22469
23781
  return ("string" === r ? String : Number)(t);
22470
23782
  }
22471
- function asyncGeneratorStep3(gen, resolve2, reject, _next, _throw, key, arg) {
23783
+ function asyncGeneratorStep3(gen, resolve3, reject, _next, _throw, key, arg) {
22472
23784
  try {
22473
23785
  var info = gen[key](arg);
22474
23786
  var value = info.value;
@@ -22477,7 +23789,7 @@ function asyncGeneratorStep3(gen, resolve2, reject, _next, _throw, key, arg) {
22477
23789
  return;
22478
23790
  }
22479
23791
  if (info.done) {
22480
- resolve2(value);
23792
+ resolve3(value);
22481
23793
  } else {
22482
23794
  Promise.resolve(value).then(_next, _throw);
22483
23795
  }
@@ -22485,13 +23797,13 @@ function asyncGeneratorStep3(gen, resolve2, reject, _next, _throw, key, arg) {
22485
23797
  function _asyncToGenerator3(fn) {
22486
23798
  return function() {
22487
23799
  var self2 = this, args = arguments;
22488
- return new Promise(function(resolve2, reject) {
23800
+ return new Promise(function(resolve3, reject) {
22489
23801
  var gen = fn.apply(self2, args);
22490
23802
  function _next(value) {
22491
- asyncGeneratorStep3(gen, resolve2, reject, _next, _throw, "next", value);
23803
+ asyncGeneratorStep3(gen, resolve3, reject, _next, _throw, "next", value);
22492
23804
  }
22493
23805
  function _throw(err) {
22494
- asyncGeneratorStep3(gen, resolve2, reject, _next, _throw, "throw", err);
23806
+ asyncGeneratorStep3(gen, resolve3, reject, _next, _throw, "throw", err);
22495
23807
  }
22496
23808
  _next(void 0);
22497
23809
  });
@@ -22505,7 +23817,7 @@ function _readChunk() {
22505
23817
  return _regeneratorRuntime3().wrap(function _callee2$(_context2) {
22506
23818
  while (1) switch (_context2.prev = _context2.next) {
22507
23819
  case 0:
22508
- return _context2.abrupt("return", new Promise(function(resolve2, reject) {
23820
+ return _context2.abrupt("return", new Promise(function(resolve3, reject) {
22509
23821
  var onError = function onError2(err) {
22510
23822
  stream.off("readable", onReadable);
22511
23823
  reject(err);
@@ -22515,7 +23827,7 @@ function _readChunk() {
22515
23827
  if (chunk2 !== null) {
22516
23828
  stream.off("error", onError);
22517
23829
  stream.off("readable", onReadable2);
22518
- resolve2(chunk2);
23830
+ resolve3(chunk2);
22519
23831
  }
22520
23832
  };
22521
23833
  stream.once("error", onError);
@@ -22696,19 +24008,19 @@ var FileReader = /* @__PURE__ */ (function() {
22696
24008
  })();
22697
24009
 
22698
24010
  // ../../node_modules/tus-js-client/lib.esm/node/fileSignature.js
22699
- import { createHash as createHash3 } from "crypto";
24011
+ import { createHash as createHash4 } from "crypto";
22700
24012
  import * as fs from "fs";
22701
24013
  import * as path from "path";
22702
24014
  function fingerprint(file, options) {
22703
24015
  if (Buffer.isBuffer(file)) {
22704
24016
  var blockSize = 64 * 1024;
22705
24017
  var content = file.slice(0, Math.min(blockSize, file.length));
22706
- var hash = createHash3("md5").update(content).digest("hex");
24018
+ var hash = createHash4("md5").update(content).digest("hex");
22707
24019
  var ret = ["node-buffer", hash, file.length, options.endpoint].join("-");
22708
24020
  return Promise.resolve(ret);
22709
24021
  }
22710
24022
  if (file instanceof fs.ReadStream && file.path != null) {
22711
- return new Promise(function(resolve2, reject) {
24023
+ return new Promise(function(resolve3, reject) {
22712
24024
  var name = path.resolve(file.path);
22713
24025
  fs.stat(file.path, function(err, info) {
22714
24026
  if (err) {
@@ -22716,7 +24028,7 @@ function fingerprint(file, options) {
22716
24028
  return;
22717
24029
  }
22718
24030
  var ret2 = ["node-file", name, info.size, info.mtime.getTime(), options.endpoint].join("-");
22719
- resolve2(ret2);
24031
+ resolve3(ret2);
22720
24032
  });
22721
24033
  });
22722
24034
  }
@@ -22908,7 +24220,7 @@ var Request = /* @__PURE__ */ (function() {
22908
24220
  value: function send() {
22909
24221
  var _this = this;
22910
24222
  var body = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : null;
22911
- return new Promise(function(resolve2, reject) {
24223
+ return new Promise(function(resolve3, reject) {
22912
24224
  var options = _objectSpread2(_objectSpread2(_objectSpread2({}, parse(_this._url)), _this._requestOptions), {}, {
22913
24225
  method: _this._method,
22914
24226
  headers: _objectSpread2(_objectSpread2({}, _this._requestOptions.headers || {}), _this._headers)
@@ -22926,7 +24238,7 @@ var Request = /* @__PURE__ */ (function() {
22926
24238
  });
22927
24239
  res.on("end", function() {
22928
24240
  var responseText = Buffer.concat(resChunks).toString("utf8");
22929
- resolve2(new Response(res, responseText));
24241
+ resolve3(new Response(res, responseText));
22930
24242
  });
22931
24243
  });
22932
24244
  req.on("error", function(err) {
@@ -23181,9 +24493,9 @@ var Upload = /* @__PURE__ */ (function(_BaseUpload) {
23181
24493
  var BUNNY_TUS_ENDPOINT = "https://video.bunnycdn.com/tusupload";
23182
24494
  var CHUNK_SIZE = 50 * 1024 * 1024;
23183
24495
  async function uploadVideoToBunny(params) {
23184
- const { size } = await stat2(params.filePath);
24496
+ const { size } = await stat4(params.filePath);
23185
24497
  const stream = createReadStream2(params.filePath);
23186
- await new Promise((resolve2, reject) => {
24498
+ await new Promise((resolve3, reject) => {
23187
24499
  const upload = new Upload(stream, {
23188
24500
  endpoint: BUNNY_TUS_ENDPOINT,
23189
24501
  uploadSize: size,
@@ -23204,7 +24516,7 @@ async function uploadVideoToBunny(params) {
23204
24516
  removeFingerprintOnSuccess: true,
23205
24517
  onError: (error) => reject(error),
23206
24518
  onProgress: (uploaded, total) => params.onProgress?.(uploaded, total),
23207
- onSuccess: () => resolve2()
24519
+ onSuccess: () => resolve3()
23208
24520
  });
23209
24521
  upload.start();
23210
24522
  });
@@ -23330,7 +24642,7 @@ function videoMimeFromName(name) {
23330
24642
  return "video/mp4";
23331
24643
  }
23332
24644
  var VIDEO_READY_STATES = /* @__PURE__ */ new Set(["ready", "failed"]);
23333
- var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
24645
+ var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
23334
24646
  function registerMediaCommands(program2) {
23335
24647
  const media = program2.command("media").description("Upload files to the workspace media library");
23336
24648
  media.command("upload").description("Upload a file (image, etc.) to the media library; prints its public URL").requiredOption("--file <path>", "path to the file to upload").option("--name <name>", "display name (defaults to the file name)").option("--mime <type>", "MIME type (inferred from the file extension by default)").option("--folder <folder>", "media library folder name").option("--description <text>", "description stored on the media item").action(async (options, command) => {
@@ -23338,7 +24650,7 @@ function registerMediaCommands(program2) {
23338
24650
  if (!options.file) {
23339
24651
  throw new Error("Provide a file via --file <path>.");
23340
24652
  }
23341
- const bytes = await readFile(options.file);
24653
+ const bytes = await readFile3(options.file);
23342
24654
  const name = options.name ?? basename(options.file);
23343
24655
  const input = {
23344
24656
  name,
@@ -23361,7 +24673,7 @@ function registerMediaCommands(program2) {
23361
24673
  if (!options.file) {
23362
24674
  throw new Error("Provide a video file via --file <path>.");
23363
24675
  }
23364
- const { size } = await stat3(options.file);
24676
+ const { size } = await stat5(options.file);
23365
24677
  const title = options.title ?? basename(options.file);
23366
24678
  const mimeType = options.mime ?? videoMimeFromName(options.file);
23367
24679
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -24082,8 +25394,8 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
24082
25394
  );
24083
25395
  if (report) {
24084
25396
  printTable(
24085
- Object.entries(report.stats).map(([stat4, value]) => ({
24086
- stat: stat4,
25397
+ Object.entries(report.stats).map(([stat6, value]) => ({
25398
+ stat: stat6,
24087
25399
  value: String(value ?? "")
24088
25400
  })),
24089
25401
  ["stat", "value"]
@@ -24677,7 +25989,8 @@ Prices (${checkout.prices.length}):
24677
25989
  hidden: yesNo(p.hidden),
24678
25990
  saleStartsAt: p.saleStartsAt ?? "",
24679
25991
  saleEndsAt: p.saleEndsAt ?? "",
24680
- quantityLimit: p.quantityLimit ?? ""
25992
+ // A stored 0 sells without limit, same as no limit at all.
25993
+ quantityLimit: p.quantityLimit || "unlimited"
24681
25994
  })),
24682
25995
  [
24683
25996
  "id",
@@ -25431,10 +26744,33 @@ function registerOtoCommands(program2) {
25431
26744
 
25432
26745
  // src/commands/pages/index.ts
25433
26746
  import { existsSync as existsSync3, readFileSync as readFileSync21 } from "fs";
25434
- import { readFile as readFile2 } from "fs/promises";
26747
+ import { readFile as readFile4 } from "fs/promises";
26748
+ var CLEAR_VALUE = "none";
26749
+ var FAMILY_STEPS = ["follow-ups", "thank-you-page", "room-page"];
26750
+ function parseClearableLink(value, flag) {
26751
+ const trimmed = value.trim();
26752
+ if (trimmed === CLEAR_VALUE) return null;
26753
+ if (!/^https?:\/\/\S+$/i.test(trimmed)) {
26754
+ throw new Error(
26755
+ `Invalid ${flag}: ${value} (expected an http(s) link, or ${CLEAR_VALUE} to clear)`
26756
+ );
26757
+ }
26758
+ return trimmed;
26759
+ }
26760
+ function buildEventInput(options) {
26761
+ const event = {
26762
+ ...options.eventStart !== void 0 ? { startAt: options.eventStart } : {},
26763
+ ...options.eventTimezone !== void 0 ? { timezone: options.eventTimezone } : {},
26764
+ ...options.eventDuration !== void 0 ? { durationMinutes: parsePositiveInt14(options.eventDuration, "--event-duration") } : {},
26765
+ ...options.joinUrl !== void 0 ? { joinUrl: parseClearableLink(options.joinUrl, "--join-url") } : {},
26766
+ ...options.replayUrl !== void 0 ? { replayUrl: parseClearableLink(options.replayUrl, "--replay-url") } : {},
26767
+ ...options.liveRoom !== void 0 ? { liveRoomId: options.liveRoom.trim() === CLEAR_VALUE ? null : options.liveRoom.trim() } : {}
26768
+ };
26769
+ return Object.keys(event).length > 0 ? event : void 0;
26770
+ }
25435
26771
  async function resolveHtml2(options) {
25436
26772
  if (options.file) {
25437
- return readFile2(options.file, "utf8");
26773
+ return readFile4(options.file, "utf8");
25438
26774
  }
25439
26775
  return options.html;
25440
26776
  }
@@ -25618,7 +26954,7 @@ function registerPagesCommands(program2) {
25618
26954
  });
25619
26955
  });
25620
26956
  pages.command("update").description(
25621
- "Update a page (slug, title, HTML body, cookie-banner visibility, or its tracking tools)"
26957
+ "Update a page (slug, title, HTML body, cookie-banner visibility, its tracking tools, or a webinar page's date and links). A new --event-start moves the reminder ladder with it (send moments, the date in the copy, rungs already scheduled); --join-url changes the watch page's button at once."
25622
26958
  ).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(
25623
26959
  "--no-cookie-banner",
25624
26960
  "hide the workspace cookie banner on this page (pixels still wait for consent given elsewhere)"
@@ -25628,9 +26964,16 @@ function registerPagesCommands(program2) {
25628
26964
  ).option(
25629
26965
  "--tracking-integrations <ids>",
25630
26966
  "comma-separated tracking integration ids for --tracking selected (one per platform)"
26967
+ ).option("--event-start <iso>", "webinar: start as ISO 8601 WITH offset (moves the reminders)").option("--event-timezone <tz>", "webinar: IANA timezone, with --event-start").option("--event-duration <minutes>", "webinar: length in minutes, with --event-start").option(
26968
+ "--join-url <url>",
26969
+ `webinar: the meeting link (ClickMeeting, Zoom, Teams, Meet); ${CLEAR_VALUE} clears it`
26970
+ ).option("--replay-url <url>", `webinar: the recording link; ${CLEAR_VALUE} clears it`).option(
26971
+ "--live-room <roomId>",
26972
+ `webinar: run it on a webinar from \`live-rooms list\` (live_rooms plan feature); ${CLEAR_VALUE} detaches`
25631
26973
  ).action(async (id, options, command) => {
25632
26974
  const flags = getGlobalFlags(command);
25633
26975
  const html = await resolveHtml2(options);
26976
+ const event = buildEventInput(options);
25634
26977
  const trackingIds = options.trackingIntegrations?.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
25635
26978
  const trackingMode = options.tracking !== void 0 ? parseChoice5(options.tracking, TRACKING_MODES, "--tracking") : trackingIds !== void 0 ? "selected" : void 0;
25636
26979
  const input = {
@@ -25643,7 +26986,8 @@ function registerPagesCommands(program2) {
25643
26986
  mode: trackingMode,
25644
26987
  ...trackingIds !== void 0 ? { integrationIds: trackingIds } : {}
25645
26988
  }
25646
- } : {}
26989
+ } : {},
26990
+ ...event ? { event } : {}
25647
26991
  };
25648
26992
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25649
26993
  const result = await client.updatePage(id, input);
@@ -25652,24 +26996,42 @@ function registerPagesCommands(program2) {
25652
26996
  `);
25653
26997
  });
25654
26998
  });
25655
- pages.command("publish").description("Publish a page").argument("<id>", "page id").action(async (id, _, command) => {
25656
- const flags = getGlobalFlags(command);
25657
- const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25658
- const result = await client.publishPage(id, true);
25659
- emit(flags, result, () => {
25660
- process.stdout.write(`Page ${result.pageId} is now ${result.status}
25661
- `);
25662
- });
25663
- });
25664
- pages.command("unpublish").description("Revert a page to draft").argument("<id>", "page id").action(async (id, _, command) => {
25665
- const flags = getGlobalFlags(command);
25666
- const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25667
- const result = await client.publishPage(id, false);
25668
- emit(flags, result, () => {
25669
- process.stdout.write(`Page ${result.pageId} is now ${result.status}
26999
+ const publishCommand = (verb, publish, description) => {
27000
+ pages.command(verb).description(description).argument("<id>", "page id").option("--only-this-page", "change this page only, not its companions or site pages").option("--dry-run", "list the pages that would change; change nothing").action(
27001
+ async (id, options, command) => {
27002
+ const flags = getGlobalFlags(command);
27003
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27004
+ const result = await client.publishPage(id, publish, {
27005
+ ...options.onlyThisPage ? { scope: "page" } : {},
27006
+ ...options.dryRun ? { dryRun: true } : {}
27007
+ });
27008
+ emit(flags, result, () => {
27009
+ if (!result.dryRun) {
27010
+ process.stdout.write(`Page ${result.pageId} is now ${result.status}
25670
27011
  `);
25671
- });
25672
- });
27012
+ return;
27013
+ }
27014
+ const changes = result.changes ?? [];
27015
+ process.stdout.write(
27016
+ changes.length === 0 ? "Dry run: nothing would change.\n" : `Dry run: ${changes.length} page(s) would change (nothing changed):
27017
+ `
27018
+ );
27019
+ for (const change of changes) {
27020
+ process.stdout.write(
27021
+ ` ${change.pageId} /${change.slug} "${change.title}": ${change.status} -> ${change.nextStatus}
27022
+ `
27023
+ );
27024
+ }
27025
+ });
27026
+ }
27027
+ );
27028
+ };
27029
+ publishCommand("publish", true, "Publish a page (a site's home page publishes the whole site)");
27030
+ publishCommand(
27031
+ "unpublish",
27032
+ false,
27033
+ "Revert a page to draft (a site's home page reverts the whole site)"
27034
+ );
25673
27035
  pages.command("delete").description("Delete a page").argument("<id>", "page id").action(async (id, _, command) => {
25674
27036
  const flags = getGlobalFlags(command);
25675
27037
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -25698,13 +27060,19 @@ function registerPagesCommands(program2) {
25698
27060
  });
25699
27061
  pages.command("generate").description(
25700
27062
  "AI-generate a hosted DRAFT page from a brief. Consumes AI credits and runs in the background (202 + run id, ~1-5 minutes) \u2014 the page appears in `pages list` when the run finishes. Signup intents (lead-magnet / webinar / waitlist) also provision a signup form, a companion thank-you page and a DRAFT delivery automation. Facts in the brief (price, date, links) are used verbatim. Requires --confirm."
25701
- ).requiredOption("--intent <intent>", `what the page is for (${PAGE_INTENTS.join(" | ")})`).requiredOption("--title <title>", "page title (the slug derives from it unless --slug)").requiredOption("--brief <textOrFile>", "the brief \u2014 inline text or a path to a text file").option("--template <templateId>", "design-template seed from `pages templates`").option("--product <productId>", "grounding product (real name/prices + checkout link)").option("--checkout <checkoutId>", "override the product's default checkout as the offer").option("--form <formId>", "standalone signup form whose embed becomes the signup CTA").option("--source-url <url>", "website to start from (fetched server-side; beats --template)").option(
27063
+ ).requiredOption("--intent <intent>", `what the page is for (${PAGE_INTENTS.join(" | ")})`).requiredOption("--title <title>", "page title (the slug derives from it unless --slug)").requiredOption("--brief <textOrFile>", "the brief \u2014 inline text or a path to a text file").option("--template <templateId>", "design-template seed from `pages templates`").option("--product <productId>", "grounding product (real name/prices + checkout link)").option("--checkout <checkoutId>", "override the product's default checkout as the offer").option(
27064
+ "--form <formId>",
27065
+ "standalone signup form the page renders natively as its signup section (never an iframe)"
27066
+ ).option("--source-url <url>", "website to start from (fetched server-side; beats --template)").option(
25702
27067
  "--source-mode <mode>",
25703
27068
  `how the source site is used, required with --source-url (${SOURCE_SITE_MODES.join(" | ")})`
25704
27069
  ).option("--slug <slug>", "public URL slug; derived from the title when omitted").option(
25705
27070
  "--delivery-url <url>",
25706
27071
  "what the signup delivers (file URL / join link) \u2014 never printed"
25707
- ).option("--delivery-name <name>", "human name of the deliverable for the delivery email").option("--event-start <iso>", "webinar: event start as ISO 8601 WITH offset").option("--event-timezone <tz>", "webinar: IANA timezone, e.g. Europe/Warsaw").option("--event-duration <minutes>", "webinar: event length in minutes (default 60)").option("--event-mode <mode>", `webinar: ${EVENT_MODES2.join(" | ")} (on_demand = autowebinar)`).option("--replay-media <mediaId>", "on-demand webinar: Media Library video the room plays").option(
27072
+ ).option("--delivery-name <name>", "human name of the deliverable for the delivery email").option("--event-start <iso>", "webinar: event start as ISO 8601 WITH offset").option("--event-timezone <tz>", "webinar: IANA timezone, e.g. Europe/Warsaw").option("--event-duration <minutes>", "webinar: event length in minutes (default 60)").option(
27073
+ "--event-mode <mode>",
27074
+ `webinar: ${EVENT_MODES2.join(" | ")} (on_demand = on-demand webinar, no date)`
27075
+ ).option("--replay-media <mediaId>", "on-demand webinar: Media Library video the room plays").option(
25708
27076
  "--opt-in-mode <mode>",
25709
27077
  `signup form opt-in (${OPT_IN_MODES2.join(" | ")}; default single)`
25710
27078
  ).option("--form-tags <ids>", "comma-separated tag ids the auto-provisioned form applies").option(
@@ -25745,6 +27113,14 @@ function registerPagesCommands(program2) {
25745
27113
  emit(flags, result, () => {
25746
27114
  process.stdout.write(`Edited page ${pageId}.
25747
27115
  `);
27116
+ const removed = result.removedCode;
27117
+ if (removed) {
27118
+ const sources = removed.scriptSources.length > 0 ? ` (${removed.scriptSources.join("; ")})` : "";
27119
+ process.stdout.write(
27120
+ ` Removed on save, NOT on the page: ${removed.scripts} script(s)${sources}, ${removed.handlers} event handler(s), ${removed.jsUrls} javascript: link(s). Put a third-party embed in the HTML yourself (\`pages update ${pageId} --file \u2026\`).
27121
+ `
27122
+ );
27123
+ }
25748
27124
  process.stdout.write(
25749
27125
  ` Previous HTML kept as the newest entry of \`pages revisions ${pageId}\` \u2014 undo with \`pages restore ${pageId} <revisionId> --confirm\`.
25750
27126
  `
@@ -25802,6 +27178,20 @@ function registerPagesCommands(program2) {
25802
27178
  });
25803
27179
  }
25804
27180
  );
27181
+ pages.command("family-step").description(
27182
+ "Build ONE missing piece of a page's family (the editor checklist's rows): follow-ups = the reminder ladder of a dated webinar page (e.g. your own page that got its date with `pages update --event-start`), thank-you-page / room-page (the watch page) = AI generations (credits). Runs in the background (1-5 min); the ladder lands as DRAFTS that `pages activate-automations` then schedules."
27183
+ ).argument("<pageId>", "the signup / webinar registration page id").requiredOption("--step <step>", `which piece (${FAMILY_STEPS.join(" | ")})`).action(async (pageId, options, command) => {
27184
+ const flags = getGlobalFlags(command);
27185
+ const step = parseChoice5(options.step, FAMILY_STEPS, "--step");
27186
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27187
+ const result = await client.startPageFamilyStep(pageId, { step });
27188
+ emit(flags, result, () => {
27189
+ process.stdout.write(
27190
+ `Queued ${step} for page ${pageId} (run ${result.runId}). It takes 1-5 minutes` + (step === "follow-ups" ? `; then run "pages activate-automations ${pageId} --confirm" to schedule the reminders.
27191
+ ` : ".\n")
27192
+ );
27193
+ });
27194
+ });
25805
27195
  pages.command("activate-automations").description(
25806
27196
  "One-click activation of everything generated around a page: publishes the DRAFT delivery automation and schedules the webinar follow-up broadcasts at their stored send times. Emails will go out for real; items whose copy still carries [FILL IN] placeholders are skipped, never sent. Requires --confirm."
25807
27197
  ).argument("<pageId>", "page id").option("--confirm", "required \u2014 acknowledge that automations go live and emails will send").action(async (pageId, options, command) => {
@@ -25837,7 +27227,7 @@ function registerPagesCommands(program2) {
25837
27227
 
25838
27228
  // src/commands/products/index.ts
25839
27229
  import { readFileSync as readFileSync22 } from "fs";
25840
- import { readFile as readFile3 } from "fs/promises";
27230
+ import { readFile as readFile5 } from "fs/promises";
25841
27231
  import { basename as basename2 } from "path";
25842
27232
  var KB_TREE_CHANGED_ERROR = "tree-changed";
25843
27233
  var JSON_FLAG_HINT3 = "(write it as --json=<value> \u2014 a bare --json is the global JSON-output flag)";
@@ -25972,7 +27362,7 @@ function collectRepeatable6(value, previous) {
25972
27362
  }
25973
27363
  var HTTP_URL_PATTERN2 = /^https?:\/\//i;
25974
27364
  async function uploadLocalFile(client, filePath) {
25975
- const bytes = await readFile3(filePath);
27365
+ const bytes = await readFile5(filePath);
25976
27366
  const name = basename2(filePath);
25977
27367
  const result = await client.uploadMedia({
25978
27368
  name,
@@ -26321,7 +27711,7 @@ function registerProductsCommands(program2) {
26321
27711
  }
26322
27712
  );
26323
27713
  plans.command("backfill").description(
26324
- "Eagerly add plan-bundled customers to a community \u2014 creates member docs for every active customer whose effective plan includes the community. Requires --confirm."
27714
+ "Eagerly add plan-bundled customers to a community \u2014 starts a background run that creates member docs for every active customer whose effective plan includes the community. Requires --confirm."
26325
27715
  ).argument("<productId>", "the parent (bundling) product id").requiredOption(
26326
27716
  "--community <communityProductId>",
26327
27717
  "the community PRODUCT id to backfill members into"
@@ -26336,8 +27726,12 @@ function registerProductsCommands(program2) {
26336
27726
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26337
27727
  const result = await client.backfillPlanCommunityMembers(productId, options.community);
26338
27728
  emit(flags, result, () => {
27729
+ if (result.status === "no-eligible-clients") {
27730
+ process.stdout.write("Nothing to backfill: no active customer is on these plans.\n");
27731
+ return;
27732
+ }
26339
27733
  process.stdout.write(
26340
- `Backfill done: ${result.added} added, ${result.alreadyMembers} already members, ${result.skipped} skipped (no auth user yet \u2014 the lazy backstop covers them).
27734
+ (result.status === "already-running" ? "The same backfill is already running" : "Backfill started in the background") + ` for ${result.eligibleClients} eligible customers (run ${result.runId}). Members appear as the run goes; customers without an auth user yet are skipped (the lazy backstop covers them).
26341
27735
  `
26342
27736
  );
26343
27737
  });
@@ -26462,7 +27856,7 @@ function registerProductsCommands(program2) {
26462
27856
  if (options.deliveryTime === void 0 !== (options.deliveryUnit === void 0)) {
26463
27857
  throw new Error("Pass --delivery-time and --delivery-unit together.");
26464
27858
  }
26465
- const duration = options.duration !== void 0 ? parseNonNegativeInt3(options.duration, "--duration") : void 0;
27859
+ const duration2 = options.duration !== void 0 ? parseNonNegativeInt3(options.duration, "--duration") : void 0;
26466
27860
  const deliveryTimeValue = options.deliveryTime !== void 0 ? parseNonNegativeInt3(options.deliveryTime, "--delivery-time") : void 0;
26467
27861
  const capacity = options.capacity !== void 0 ? parseNonNegativeInt3(options.capacity, "--capacity") : void 0;
26468
27862
  let revisionRounds;
@@ -26486,7 +27880,7 @@ function registerProductsCommands(program2) {
26486
27880
  const config2 = {
26487
27881
  bookingMethod: bookingMethod ?? base2.bookingMethod ?? "external",
26488
27882
  bookingUrl: options.bookingUrl ?? base2.bookingUrl ?? null,
26489
- duration: duration ?? base2.duration ?? 0,
27883
+ duration: duration2 ?? base2.duration ?? 0,
26490
27884
  location: options.location ?? base2.location ?? "",
26491
27885
  locationDetails: options.locationDetails ?? base2.locationDetails ?? null,
26492
27886
  confirmationMessage: options.confirmationMessage ?? base2.confirmationMessage ?? null,
@@ -27014,6 +28408,10 @@ var PROGRAM_KINDS = ["referral", "affiliate"];
27014
28408
  var WRITABLE_STATUSES = ["active", "inactive"];
27015
28409
  var ENROLLMENT_MODES = ["open", "inviteOnly", "closed"];
27016
28410
  var REWARD_DECISIONS = ["approve", "reject"];
28411
+ var SWITCH_VALUES = ["on", "off"];
28412
+ function parseSwitch(value, flag) {
28413
+ return parseChoice6(value, SWITCH_VALUES, flag) === "on";
28414
+ }
27017
28415
  function parseChoice6(value, allowed, flag) {
27018
28416
  const match = allowed.find((candidate) => candidate === value);
27019
28417
  if (match === void 0) {
@@ -27094,6 +28492,10 @@ function printProgram(program2) {
27094
28492
  `);
27095
28493
  process.stdout.write(` Products: ${program2.productIds.join(", ")}
27096
28494
  `);
28495
+ if (program2.kind === "referral") {
28496
+ process.stdout.write(` Form signups count: ${program2.countFormSignups ? "on" : "off"}
28497
+ `);
28498
+ }
27097
28499
  process.stdout.write(
27098
28500
  ` Attribution: ${program2.attribution.windowDays}-day window, coupon attribution ${program2.attribution.allowCouponAttribution ? "on" : "off"}, two-sided discount ${program2.attribution.twoSidedDiscountId ?? "none"}
27099
28501
  `
@@ -27154,6 +28556,9 @@ function buildCreateInput(options) {
27154
28556
  if (options.status !== void 0) {
27155
28557
  input.status = parseChoice6(options.status, WRITABLE_STATUSES, "--status");
27156
28558
  }
28559
+ if (options.countFormSignups !== void 0) {
28560
+ input.countFormSignups = parseSwitch(options.countFormSignups, "--count-form-signups");
28561
+ }
27157
28562
  return input;
27158
28563
  }
27159
28564
  function buildUpdateInput2(options) {
@@ -27166,9 +28571,12 @@ function buildUpdateInput2(options) {
27166
28571
  if (options.enrollment !== void 0) {
27167
28572
  input.enrollment = parseChoice6(options.enrollment, ENROLLMENT_MODES, "--enrollment");
27168
28573
  }
28574
+ if (options.countFormSignups !== void 0) {
28575
+ input.countFormSignups = parseSwitch(options.countFormSignups, "--count-form-signups");
28576
+ }
27169
28577
  if (Object.keys(input).length === 0) {
27170
28578
  throw new Error(
27171
- "Nothing to update \u2014 pass --name, --status, --product, --enrollment or --json <config>."
28579
+ "Nothing to update \u2014 pass --name, --status, --product, --enrollment, --count-form-signups or --json <config>."
27172
28580
  );
27173
28581
  }
27174
28582
  return input;
@@ -27276,6 +28684,9 @@ Failed deliveries (${result.failedDeliveries.length})
27276
28684
  referrals.command("create").description(
27277
28685
  "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."
27278
28686
  ).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(
28687
+ "--count-form-signups <on|off>",
28688
+ "referral kind: confirmed form signups of new contacts with a participant code count too (default off)"
28689
+ ).option(
27279
28690
  "--json <fileOrJson>",
27280
28691
  "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)"
27281
28692
  ).action(async (options, command) => {
@@ -27291,6 +28702,9 @@ Failed deliveries (${result.failedDeliveries.length})
27291
28702
  referrals.command("update").description(
27292
28703
  "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."
27293
28704
  ).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(
28705
+ "--count-form-signups <on|off>",
28706
+ "referral kind: confirmed form signups of new contacts with a participant code count too (default off)"
28707
+ ).option(
27294
28708
  "--json <fileOrJson>",
27295
28709
  "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)"
27296
28710
  ).action(async (programId, options, command) => {
@@ -27397,9 +28811,9 @@ function parseMinorUnits(value, flag) {
27397
28811
  }
27398
28812
  return n;
27399
28813
  }
27400
- var CONDITION_FLAGS_HELP = "--has-tag/--without-tag, --has-any-tag/--has-none-tag, --owns/--not-owns, --owns-direct/--not-owns-direct, --spent-gte/--spent-lte, --subscription, or --json-file";
28814
+ var CONDITION_FLAGS_HELP = "--has-tag/--without-tag, --has-any-tag/--has-all-tag/--has-none-tag, --owns/--not-owns, --owns-direct/--not-owns-direct, --spent-gte/--spent-lte, --subscription, --referral-participant/--not-referral-participant, or --json-file";
27401
28815
  function resolveConditions(options) {
27402
- const hasFlagConditions = options.hasTag.length > 0 || options.withoutTag.length > 0 || options.hasAnyTag.length > 0 || options.hasNoneTag.length > 0 || options.owns.length > 0 || options.notOwns.length > 0 || options.ownsDirect.length > 0 || options.notOwnsDirect.length > 0 || options.spentGte !== void 0 || options.spentLte !== void 0 || options.subscription !== void 0;
28816
+ const hasFlagConditions = options.hasTag.length > 0 || options.withoutTag.length > 0 || options.hasAnyTag.length > 0 || options.hasAllTag.length > 0 || options.hasNoneTag.length > 0 || options.owns.length > 0 || options.notOwns.length > 0 || options.ownsDirect.length > 0 || options.notOwnsDirect.length > 0 || options.spentGte !== void 0 || options.spentLte !== void 0 || options.subscription !== void 0 || options.referralParticipant.length > 0 || options.notReferralParticipant.length > 0;
27403
28817
  if (options.jsonFile !== void 0) {
27404
28818
  if (hasFlagConditions) {
27405
28819
  throw new Error("Use either condition flags or --json-file, not both.");
@@ -27429,6 +28843,9 @@ function resolveConditions(options) {
27429
28843
  if (options.hasAnyTag.length > 0) {
27430
28844
  conditions.push({ field: "tag", operator: "has-any", tagIds: options.hasAnyTag });
27431
28845
  }
28846
+ if (options.hasAllTag.length > 0) {
28847
+ conditions.push({ field: "tag", operator: "has-all", tagIds: options.hasAllTag });
28848
+ }
27432
28849
  if (options.hasNoneTag.length > 0) {
27433
28850
  conditions.push({ field: "tag", operator: "has-none", tagIds: options.hasNoneTag });
27434
28851
  }
@@ -27473,6 +28890,12 @@ function resolveConditions(options) {
27473
28890
  if (options.notClicked !== void 0) {
27474
28891
  conditions.push({ field: "campaign", operator: "not-clicked", campaignId: options.notClicked });
27475
28892
  }
28893
+ for (const programId of options.referralParticipant) {
28894
+ conditions.push({ field: "referralProgram", operator: "participant", programId });
28895
+ }
28896
+ for (const programId of options.notReferralParticipant) {
28897
+ conditions.push({ field: "referralProgram", operator: "not-participant", programId });
28898
+ }
27476
28899
  return conditions;
27477
28900
  }
27478
28901
  function withConditionOptions(command) {
@@ -27491,6 +28914,11 @@ function withConditionOptions(command) {
27491
28914
  "client carries AT LEAST ONE of these tags (repeat the flag; one OR condition, max 10 ids) - one send to several tags without double-mailing",
27492
28915
  collectRepeatable7,
27493
28916
  []
28917
+ ).option(
28918
+ "--has-all-tag <tagId>",
28919
+ "client carries EVERY ONE of these tags (repeat the flag; one condition, max 10 ids)",
28920
+ collectRepeatable7,
28921
+ []
27494
28922
  ).option(
27495
28923
  "--has-none-tag <tagId>",
27496
28924
  "client carries NONE of these tags (repeat the flag; one condition, max 10 ids)",
@@ -27528,6 +28956,16 @@ function withConditionOptions(command) {
27528
28956
  ).option(
27529
28957
  "--not-clicked <campaignId>",
27530
28958
  "received this SENT campaign and never clicked a link in it"
28959
+ ).option(
28960
+ "--referral-participant <programId>",
28961
+ "client is an ACTIVE participant of this referral program (rprog_\u2026; blocked ones do not count; repeatable)",
28962
+ collectRepeatable7,
28963
+ []
28964
+ ).option(
28965
+ "--not-referral-participant <programId>",
28966
+ "client is NOT an active participant of this referral program (repeatable)",
28967
+ collectRepeatable7,
28968
+ []
27531
28969
  ).option(
27532
28970
  "--json-file <path>",
27533
28971
  "read the conditions as a JSON array from a file instead of flags"
@@ -27537,7 +28975,12 @@ function describeCondition(condition) {
27537
28975
  switch (condition.field) {
27538
28976
  case "tag":
27539
28977
  if ("tagIds" in condition) {
27540
- return `${condition.operator === "has-any" ? "has any of" : "has none of"} tags ${condition.tagIds.join(", ")}`;
28978
+ const listWords = {
28979
+ "has-any": "has any of",
28980
+ "has-all": "has all of",
28981
+ "has-none": "has none of"
28982
+ };
28983
+ return `${listWords[condition.operator]} tags ${condition.tagIds.join(", ")}`;
27541
28984
  }
27542
28985
  return `${condition.operator === "has" ? "has" : "lacks"} tag ${condition.tagId}`;
27543
28986
  case "product":
@@ -27550,12 +28993,17 @@ function describeCondition(condition) {
27550
28993
  return `last email engagement ${condition.operator === "within" ? "within" : "not within"} the last ${condition.days} days`;
27551
28994
  case "lastOrderAt":
27552
28995
  return `last paid order ${condition.operator === "within" ? "within" : "not within"} the last ${condition.days} days`;
28996
+ case "emailSubscribedAt":
28997
+ case "addedAt":
28998
+ return `${condition.field === "emailSubscribedAt" ? "subscribed to the newsletter" : "added"} ${condition.operator === "within" ? `in the last ${condition.days} days` : `at least ${condition.days} days ago`}`;
27553
28999
  case "smsSubscription":
27554
29000
  return `sms consent is ${condition.status}`;
27555
29001
  case "phoneNumber":
27556
29002
  return condition.operator === "has" ? "has a phone number" : "has no phone number";
27557
29003
  case "campaign":
27558
29004
  return `${condition.operator.replace("-", " ")} campaign ${condition.campaignId}`;
29005
+ case "referralProgram":
29006
+ return `${condition.operator === "participant" ? "takes part in" : "does not take part in"} referral program ${condition.programId}`;
27559
29007
  case "email":
27560
29008
  return `email ${condition.operator === "is-any" ? "is one of" : "is none of"} ${condition.emails.join(", ")} (set in the dashboard)`;
27561
29009
  }
@@ -27915,7 +29363,7 @@ function withBrandingHint(error) {
27915
29363
  }
27916
29364
  var SECTION_UPDATED = (section) => `Updated site ${section} settings.
27917
29365
  `;
27918
- function registerSiteCommands(program2) {
29366
+ function registerSiteCommands2(program2) {
27919
29367
  const site = program2.command("site").description(
27920
29368
  "Manage the public site \u2014 platform settings (general, theme, branding, main page, privacy, portal, access policy, communication), activation, slug, custom domains, storefront layout and legal documents"
27921
29369
  );
@@ -28282,7 +29730,9 @@ function registerSiteCommands(program2) {
28282
29730
  const result = await client.verifySiteDomains(list);
28283
29731
  emit(flags, result, () => printDomainResults(result.results));
28284
29732
  });
28285
- const storefront = site.command("storefront").description("The product storefront layout: sections, preset, theme mode, footer");
29733
+ const storefront = site.command("storefront").description(
29734
+ "The product storefront layout: sections, preset, theme mode, profile hero, footer"
29735
+ );
28286
29736
  storefront.command("get").description("Storefront config (sections table; `--json` for the full config)").action(async (_options, command) => {
28287
29737
  const flags = getGlobalFlags(command);
28288
29738
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -28293,7 +29743,7 @@ function registerSiteCommands(program2) {
28293
29743
  "REPLACE the whole storefront config with --json=<file|inline> (read `site storefront get --json` first and edit that)"
28294
29744
  ).option(
28295
29745
  "--json <fileOrJson>",
28296
- "inline JSON object or .json path \u2014 sections (required), layout, columns, preset, mode, overrides, looseProductOrder, heading, subheading, footer"
29746
+ "inline JSON object or .json path \u2014 sections (required), layout, columns, preset, mode, overrides, looseProductOrder, heading, subheading, hero, footer"
28297
29747
  ).action(async (options, command) => {
28298
29748
  const flags = getGlobalFlags(command);
28299
29749
  if (options.json === void 0) {
@@ -28370,6 +29820,7 @@ function printStorefront(config) {
28370
29820
  { field: "columns", value: config.columns ?? "" },
28371
29821
  { field: "heading", value: config.heading ?? "" },
28372
29822
  { field: "subheading", value: config.subheading ?? "" },
29823
+ { field: "hasHero", value: yesNo2(Boolean(config.hero)) },
28373
29824
  { field: "hasOverrides", value: yesNo2(Boolean(config.overrides)) },
28374
29825
  { field: "hasFooter", value: yesNo2(Boolean(config.footer)) },
28375
29826
  { field: "looseProductOrder", value: (config.looseProductOrder ?? []).length }
@@ -28384,10 +29835,11 @@ function printStorefront(config) {
28384
29835
  layout: section.layout,
28385
29836
  mode: section.mode,
28386
29837
  products: section.mode === "manual" ? (section.productIds ?? []).length : "",
29838
+ links: section.mode === "manual" ? (section.items ?? []).filter((item) => item.type === "link").length : "",
28387
29839
  rule: section.rule ? `${section.rule.type}${section.rule.productType ? `:${section.rule.productType}` : ""}${section.rule.tag ? `:${section.rule.tag}` : ""}` : "",
28388
29840
  sort: section.sort ?? ""
28389
29841
  })),
28390
- ["id", "title", "layout", "mode", "products", "rule", "sort"]
29842
+ ["id", "title", "layout", "mode", "products", "links", "rule", "sort"]
28391
29843
  );
28392
29844
  process.stdout.write(
28393
29845
  "\nFull config to edit and send back: `zanfia --json site storefront get`.\n"
@@ -28690,7 +30142,210 @@ Next page: --cursor ${page.nextCursor}
28690
30142
  );
28691
30143
  }
28692
30144
 
30145
+ // src/commands/social/channels.ts
30146
+ var clientOf = (command) => {
30147
+ const flags = getGlobalFlags(command);
30148
+ return { flags, client: createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose }) };
30149
+ };
30150
+ var parseCount = (value, flag) => {
30151
+ if (value === void 0) return void 0;
30152
+ const parsed = Number.parseInt(value, 10);
30153
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 50) {
30154
+ throw new Error(`${flag} must be an integer between 1 and 50.`);
30155
+ }
30156
+ return parsed;
30157
+ };
30158
+ var count = (value) => value === null || value === void 0 ? "-" : value.toLocaleString("en-US");
30159
+ var watchTime = (minutes2) => `${Math.floor(minutes2 / 60)}h ${String(Math.round(minutes2 % 60)).padStart(2, "0")}m`;
30160
+ var duration = (seconds) => `${Math.floor(seconds / 60)}:${String(Math.round(seconds % 60)).padStart(2, "0")}`;
30161
+ var videoRows = (videos) => videos.map((v) => ({
30162
+ videoId: v.videoId,
30163
+ when: (v.liveStartedAt ?? v.publishedAt).slice(0, 16).replace("T", " "),
30164
+ live: v.liveStartedAt ? "live" : "",
30165
+ views: count(v.viewCount),
30166
+ likes: count(v.likeCount),
30167
+ comments: count(v.commentCount),
30168
+ title: v.title.length > 50 ? `${v.title.slice(0, 47)}\u2026` : v.title
30169
+ }));
30170
+ var VIDEO_COLUMNS = ["videoId", "when", "live", "views", "likes", "comments", "title"];
30171
+ var write = (line) => {
30172
+ process.stdout.write(`${line}
30173
+ `);
30174
+ };
30175
+ function registerSocialChannelCommands(social) {
30176
+ const youtube = social.command("youtube").description("Connected YouTube channel: stats, analytics, videos, transcripts");
30177
+ youtube.command("stats").description("Channel counters + the 12 latest uploads (lives included)").action(async (_, command) => {
30178
+ const { flags, client } = clientOf(command);
30179
+ const response = await client.getYouTubeChannel();
30180
+ emit(flags, response, () => {
30181
+ const { channel } = response;
30182
+ write(`${channel.title ?? channel.channelId}`);
30183
+ write(` Subscribers: ${count(channel.subscriberCount)}`);
30184
+ write(` Views: ${count(channel.viewCount)}`);
30185
+ write(` Videos: ${count(channel.videoCount)}`);
30186
+ write("");
30187
+ printTable(videoRows(response.recentVideos), VIDEO_COLUMNS);
30188
+ });
30189
+ });
30190
+ youtube.command("analytics").description("Last 28 days of YouTube Analytics (needs the analytics grant)").action(async (_, command) => {
30191
+ const { flags, client } = clientOf(command);
30192
+ const response = await client.getYouTubeChannelAnalytics();
30193
+ emit(flags, response, () => {
30194
+ const { summary } = response;
30195
+ write(`YouTube Analytics ${summary.startDate} \u2192 ${summary.endDate}`);
30196
+ write(` Views: ${count(summary.views)}`);
30197
+ write(` Watch time: ${watchTime(summary.watchTimeMinutes)}`);
30198
+ write(` Average view duration: ${duration(summary.averageViewDurationSeconds)}`);
30199
+ write(` Subscribers: +${summary.subscribersGained} / -${summary.subscribersLost}`);
30200
+ });
30201
+ });
30202
+ youtube.command("videos").description("Uploads newest first (lives included) with public counters").option("--max <n>", "videos per page (default 24, max 50)").option("--page-token <token>", "nextPageToken of the previous page").action(async (options, command) => {
30203
+ const { flags, client } = clientOf(command);
30204
+ const maxResults = parseCount(options.max, "--max");
30205
+ const response = await client.listYouTubeVideos({
30206
+ ...maxResults !== void 0 ? { maxResults } : {},
30207
+ ...options.pageToken ? { pageToken: options.pageToken } : {}
30208
+ });
30209
+ emit(flags, response, () => {
30210
+ printTable(videoRows(response.videos), VIDEO_COLUMNS);
30211
+ if (response.nextPageToken) write(`
30212
+ Next page: --page-token ${response.nextPageToken}`);
30213
+ });
30214
+ });
30215
+ youtube.command("video").description("One video: views, likes, comments + watch time and average view duration").argument("<videoId>", "YouTube video id (watch?v=<id>) of a connected-channel video").action(async (videoId, _, command) => {
30216
+ const { flags, client } = clientOf(command);
30217
+ const response = await client.getYouTubeVideo(videoId);
30218
+ emit(flags, response, () => {
30219
+ const { video, analytics } = response;
30220
+ write(`${video.title} (${video.videoId})`);
30221
+ if (video.liveStartedAt) write(` Live started: ${video.liveStartedAt}`);
30222
+ else write(` Published: ${video.publishedAt}`);
30223
+ write(` Views (public counter): ${count(video.viewCount)}`);
30224
+ write(` Likes: ${count(video.likeCount)} Comments: ${count(video.commentCount)}`);
30225
+ if (analytics.status === "ok" /* Ok */) {
30226
+ write(` Analytics views: ${count(analytics.views)}`);
30227
+ write(` Watch time: ${watchTime(analytics.watchTimeMinutes)}`);
30228
+ write(` Average view duration: ${duration(analytics.averageViewDurationSeconds)}`);
30229
+ } else {
30230
+ write(` Analytics: ${analytics.message}`);
30231
+ }
30232
+ });
30233
+ });
30234
+ youtube.command("transcript").description("Plain-text transcript from the manually created captions of a video").argument("<videoId>", "YouTube video id of a connected-channel video").action(async (videoId, _, command) => {
30235
+ const { flags, client } = clientOf(command);
30236
+ const response = await client.getYouTubeVideoTranscript(videoId);
30237
+ emit(flags, response, () => {
30238
+ if (response.status === "ok" /* Ok */) {
30239
+ write(response.transcript);
30240
+ return;
30241
+ }
30242
+ write(
30243
+ response.reason === "no-manual-captions" /* NoManualCaptions */ ? "No downloadable captions: YouTube only hands out manually created ones. Upload the file to the media library and transcribe it there instead." : "This connection cannot read captions yet: reconnect the channel in the dashboard."
30244
+ );
30245
+ });
30246
+ });
30247
+ const instagram = social.command("instagram").description("Connected Instagram account: insights and posts");
30248
+ instagram.command("insights").description("Reach and views over the last 30 days").action(async (_, command) => {
30249
+ const { flags, client } = clientOf(command);
30250
+ const response = await client.getInstagramInsights();
30251
+ emit(flags, response, () => {
30252
+ if (!response.available) {
30253
+ write(`Insights unavailable: ${response.reason}`);
30254
+ return;
30255
+ }
30256
+ write("Instagram, last 30 days");
30257
+ write(` Reach: ${count(response.reach)}`);
30258
+ write(` Views: ${count(response.views)}`);
30259
+ });
30260
+ });
30261
+ instagram.command("media").description("Recent posts and reels newest first").option("--limit <n>", "items per page (default 24, max 50)").option("--after <cursor>", "nextCursor of the previous page").action(async (options, command) => {
30262
+ const { flags, client } = clientOf(command);
30263
+ const limit = parseCount(options.limit, "--limit");
30264
+ const response = await client.listInstagramMedia({
30265
+ ...limit !== void 0 ? { limit } : {},
30266
+ ...options.after ? { after: options.after } : {}
30267
+ });
30268
+ emit(flags, response, () => {
30269
+ printTable(
30270
+ response.items.map((item) => ({
30271
+ id: item.id,
30272
+ when: item.timestamp.slice(0, 16).replace("T", " "),
30273
+ type: item.mediaProductType ?? item.mediaType,
30274
+ likes: count(item.likeCount),
30275
+ comments: count(item.commentsCount),
30276
+ caption: (item.caption ?? "").replace(/\s+/g, " ").slice(0, 50)
30277
+ })),
30278
+ ["id", "when", "type", "likes", "comments", "caption"]
30279
+ );
30280
+ if (response.nextCursor) write(`
30281
+ Next page: --after ${response.nextCursor}`);
30282
+ });
30283
+ });
30284
+ social.command("threads").description("Connected Threads profile").command("posts").description("Recent Threads posts newest first").action(async (_, command) => {
30285
+ const { flags, client } = clientOf(command);
30286
+ const response = await client.listThreadsPosts();
30287
+ emit(flags, response, () => {
30288
+ printTable(
30289
+ response.items.map((post) => ({
30290
+ id: post.id,
30291
+ when: (post.timestamp ?? "").slice(0, 16).replace("T", " "),
30292
+ text: (post.text ?? "").replace(/\s+/g, " ").slice(0, 60),
30293
+ permalink: post.permalink ?? ""
30294
+ })),
30295
+ ["id", "when", "text", "permalink"]
30296
+ );
30297
+ });
30298
+ });
30299
+ social.command("snapshot").description("Latest weekly audience snapshot: followers per channel (read-only)").action(async (_, command) => {
30300
+ const { flags, client } = clientOf(command);
30301
+ const response = await client.getLatestSocialSnapshot();
30302
+ emit(flags, response, () => {
30303
+ const { snapshot } = response;
30304
+ if (!snapshot) {
30305
+ write("No audience snapshot in the last 90 days (capture one in the dashboard).");
30306
+ return;
30307
+ }
30308
+ write(`Audience snapshot ${snapshot.date}`);
30309
+ printTable(
30310
+ Object.entries(snapshot.counts).map(([channel, followers]) => ({
30311
+ channel,
30312
+ followers: count(followers),
30313
+ source: snapshot.source[channel] ?? ""
30314
+ })),
30315
+ ["channel", "followers", "source"]
30316
+ );
30317
+ for (const [channel, reason] of Object.entries(snapshot.errors)) {
30318
+ write(` ${channel}: not captured (${reason})`);
30319
+ }
30320
+ });
30321
+ });
30322
+ }
30323
+
28693
30324
  // src/commands/social/index.ts
30325
+ function updateInput(options) {
30326
+ const targets = splitList(options.targets);
30327
+ const mediaIds = splitList(options.media);
30328
+ if (options.media !== void 0 && options.clearMedia) {
30329
+ throw new Error("Pass either --media or --clear-media, not both.");
30330
+ }
30331
+ if (options.firstComment !== void 0 && options.clearFirstComment) {
30332
+ throw new Error("Pass either --first-comment or --clear-first-comment, not both.");
30333
+ }
30334
+ const input = {
30335
+ ...options.text !== void 0 ? { text: options.text } : {},
30336
+ ...targets.length > 0 ? { targets } : {},
30337
+ ...mediaIds.length > 0 ? { mediaIds } : {},
30338
+ ...options.clearMedia ? { mediaIds: [] } : {},
30339
+ ...options.firstComment !== void 0 ? { firstComment: options.firstComment } : {},
30340
+ ...options.clearFirstComment ? { firstComment: null } : {}
30341
+ };
30342
+ if (Object.keys(input).length === 0) {
30343
+ throw new Error(
30344
+ "Nothing to update: pass --text, --targets, --media, --clear-media, --first-comment or --clear-first-comment."
30345
+ );
30346
+ }
30347
+ return input;
30348
+ }
28694
30349
  var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
28695
30350
  function composeInput(options) {
28696
30351
  if (!options.text) {
@@ -28711,7 +30366,9 @@ function composeInput(options) {
28711
30366
  };
28712
30367
  }
28713
30368
  function registerSocialCommands(program2) {
28714
- const social = program2.command("social").description("Social media posts \u2014 connected accounts, drafts, scheduling, publishing");
30369
+ const social = program2.command("social").description(
30370
+ "Social media \u2014 connected accounts, posts (drafts, scheduling, publishing) and the stats of the connected accounts (youtube / instagram / threads / snapshot)"
30371
+ );
28715
30372
  social.command("accounts").description("List connected social accounts (the values to pass as --targets)").action(async (_, command) => {
28716
30373
  const flags = getGlobalFlags(command);
28717
30374
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -28723,13 +30380,14 @@ function registerSocialCommands(program2) {
28723
30380
  network: a.network,
28724
30381
  label: a.label,
28725
30382
  schedule: a.supportsScheduling ? "yes" : "no",
28726
- publishNow: a.supportsImmediatePublish ? "yes" : "no"
30383
+ publishNow: a.supportsImmediatePublish ? "yes" : "no",
30384
+ firstComment: a.firstCommentNeedsReconnect ? "reconnect" : ""
28727
30385
  })),
28728
- ["target", "network", "label", "schedule", "publishNow"]
30386
+ ["target", "network", "label", "schedule", "publishNow", "firstComment"]
28729
30387
  );
28730
30388
  });
28731
30389
  });
28732
- social.command("posts").description("List social posts (drafts, scheduled, published) newest first").option("--status <status>", "draft | scheduled | published | failed").option("--limit <n>", "max rows (default 25, max 100)").action(async (options, command) => {
30390
+ social.command("posts").description("List social posts (drafts, scheduled, published) newest first").option("--status <status>", "draft | scheduled | published | partially_failed | failed").option("--limit <n>", "max rows (default 25, max 100)").action(async (options, command) => {
28733
30391
  const flags = getGlobalFlags(command);
28734
30392
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
28735
30393
  const response = await client.listSocialPosts({
@@ -28820,6 +30478,44 @@ function registerSocialCommands(program2) {
28820
30478
  }
28821
30479
  });
28822
30480
  });
30481
+ social.command("update").description(
30482
+ "Edit a draft or scheduled post (a scheduled one stays at its time); unset options keep their value"
30483
+ ).argument("<id>", "post id").option("--text <text>", "new post text").option("--targets <list>", "comma-separated targets (see `social accounts`)").option(
30484
+ "--media <ids>",
30485
+ "comma-separated Media Library ids, in slide order (replaces the media)"
30486
+ ).option("--clear-media", "remove all media").option("--first-comment <text>", "new first comment").option("--clear-first-comment", "remove the first comment").action(async (id, options, command) => {
30487
+ const flags = getGlobalFlags(command);
30488
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
30489
+ const post = await client.updateSocialPost(id, updateInput(options));
30490
+ emit(flags, post, () => {
30491
+ process.stdout.write(
30492
+ `Updated ${post.id} (${post.status}${post.scheduledAt ? `, ${post.scheduledAt}` : ""})
30493
+ `
30494
+ );
30495
+ });
30496
+ });
30497
+ social.command("unschedule").description("Move a scheduled post back to drafts (it will not be published at its time)").argument("<id>", "scheduled post id").action(async (id, _, command) => {
30498
+ const flags = getGlobalFlags(command);
30499
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
30500
+ const post = await client.unscheduleSocialPost(id);
30501
+ emit(flags, post, () => {
30502
+ process.stdout.write(`Moved ${post.id} back to drafts
30503
+ `);
30504
+ });
30505
+ });
30506
+ social.command("delete").description(
30507
+ "Delete a post (draft, scheduled, or a published post's record: it stays live on the network). Requires --confirm."
30508
+ ).argument("<id>", "post id").option("--confirm", "required \u2014 acknowledge the post is deleted for good").action(async (id, options, command) => {
30509
+ requireConfirm(options.confirm, `delete social post ${id}`);
30510
+ const flags = getGlobalFlags(command);
30511
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
30512
+ const result = await client.deleteSocialPost(id);
30513
+ emit(flags, result, () => {
30514
+ process.stdout.write(`Deleted ${result.postId} (was ${result.previousStatus})
30515
+ `);
30516
+ });
30517
+ });
30518
+ registerSocialChannelCommands(social);
28823
30519
  }
28824
30520
 
28825
30521
  // src/commands/stats/index.ts
@@ -30271,6 +31967,16 @@ var GRAPH_NODE_KINDS = /* @__PURE__ */ new Set([
30271
31967
  "delay"
30272
31968
  ]);
30273
31969
  var GRAPH_CONDITION_BRANCHES = ["true", "false"];
31970
+ var GRAPH_GOAL_LABELS = {
31971
+ tagAdded: "gets a tag",
31972
+ anyProductPurchased: "buys any product",
31973
+ productPurchased: "buys a chosen product"
31974
+ };
31975
+ function describeGoal(goal) {
31976
+ const ids = goal.tagIds ?? goal.productIds ?? [];
31977
+ const label = GRAPH_GOAL_LABELS[goal.kind] ?? goal.kind;
31978
+ return `Goal: ${label}${ids.length > 0 ? ` (${ids.join(", ")})` : ""}, the run ends at once`;
31979
+ }
30274
31980
  var EXCERPT_LENGTH = 120;
30275
31981
  function isRecord3(value) {
30276
31982
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -30420,6 +32126,10 @@ function walkGraph(graph, templateNames) {
30420
32126
  lines.push("Not reachable from any trigger (never runs):");
30421
32127
  for (const node of orphans) lines.push(` ${describeNode(node, templateNames)}`);
30422
32128
  }
32129
+ if (graph.goal) {
32130
+ if (lines.length > 0) lines.push("");
32131
+ lines.push(describeGoal(graph.goal));
32132
+ }
30423
32133
  return lines;
30424
32134
  }
30425
32135
  function describeNodeRef(node) {
@@ -30519,8 +32229,13 @@ function describeEmail(node, templateNames) {
30519
32229
  }
30520
32230
  function describeCondition2(config) {
30521
32231
  const rules = Array.isArray(config.rules) ? config.rules.filter(isRecord3) : [];
30522
- if (rules.length === 0) return "(no rules)";
30523
- return rules.map(describeRule).join(" AND ");
32232
+ const groups2 = Array.isArray(config.groups) ? config.groups.filter(isRecord3) : [];
32233
+ if (rules.length === 0 && groups2.length === 0) return "(no rules)";
32234
+ const match = optionalString(config.match);
32235
+ return [
32236
+ ...rules.map(describeRule),
32237
+ ...groups2.map((group) => `(${describeCondition2(group)})`)
32238
+ ].join(match === "any" ? " OR " : " AND ");
30524
32239
  }
30525
32240
  function describeRule(rule) {
30526
32241
  const field = optionalString(rule.field) ?? "?";
@@ -30598,7 +32313,7 @@ function printLogins(profiles) {
30598
32313
  const row = toLoginRow(profile);
30599
32314
  const name = row.workspaceName ?? "(unnamed)";
30600
32315
  const slug = row.workspaceSlug ? ` [${row.workspaceSlug}]` : "";
30601
- const member = row.kind === "member" ? " (community member)" : "";
32316
+ const member = row.kind === "member" ? " (community member)" : row.kind === "agent" ? " (community agent key)" : "";
30602
32317
  const expires = row.expiresAt ? `, expires ${row.expiresAt.slice(0, 10)}` : "";
30603
32318
  process.stdout.write(
30604
32319
  `${row.active ? "*" : " "} ${name}${slug}${member} - ${row.workspaceId ?? row.key}${row.userEmail ? `, ${row.userEmail}` : ""}${expires}
@@ -30763,8 +32478,8 @@ function applyShortHelp(program2) {
30763
32478
 
30764
32479
  // src/program.ts
30765
32480
  function resolveCliVersion() {
30766
- if ("0.10.0".length > 0) {
30767
- return "0.10.0";
32481
+ if ("0.21.0".length > 0) {
32482
+ return "0.21.0";
30768
32483
  }
30769
32484
  try {
30770
32485
  const raw = readFileSync28(new URL("../package.json", import.meta.url), "utf8");
@@ -30791,13 +32506,14 @@ Signing in \u2014 two roles, two consent pages:
30791
32506
  ${programName} login
30792
32507
  Creator (workspace) login. Opens the consent page in the Zanfia dashboard.
30793
32508
  Grants the full Workspace API: products, clients, orders, stats, \u2026
30794
- ${programName} login --address https://<creator-platform-url>
32509
+ ${programName} login --address https://<creator-platform-url> [--read-only]
30795
32510
  Community member login. Opens the consent page on that creator's platform.
30796
- Grants the Community API only: communities, posts, comments.
32511
+ Grants the Community API only: communities, posts, comments. --read-only
32512
+ gets a key that reads and summarizes but never posts.
30797
32513
  ${programName} auth set-key --api-url <api-community-url> <key>
30798
32514
  Member alternative without a browser: paste the key generated on the
30799
- creator's platform under Settings \u2192 Developer.
30800
- ${programName} join <handle>
32515
+ creator's platform under Settings \u2192 AI agents.
32516
+ ${programName} join <handle> [--read-only]
30801
32517
  Join a public community from the terminal: emailed 6-digit code, then a
30802
32518
  member key is saved \u2014 no account or browser needed. Try: join crew-of-one
30803
32519
  `
@@ -30811,7 +32527,7 @@ Signing in \u2014 two roles, two consent pages:
30811
32527
  registerOffersCommands(program2);
30812
32528
  registerCheckoutSettingsCommands(program2);
30813
32529
  registerIntegrationsCommands(program2);
30814
- registerSiteCommands(program2);
32530
+ registerSiteCommands2(program2);
30815
32531
  registerEmailsCommands(program2);
30816
32532
  registerOtoCommands(program2);
30817
32533
  registerBumpsCommands(program2);
@@ -30841,6 +32557,7 @@ Signing in \u2014 two roles, two consent pages:
30841
32557
  registerPagesCommands(program2);
30842
32558
  registerSocialCommands(program2);
30843
32559
  registerMediaCommands(program2);
32560
+ registerFilesCommands(program2);
30844
32561
  applyGlobalOptions(program2);
30845
32562
  applyShortHelp(program2);
30846
32563
  return program2;