@zanfia/cli 0.21.0 → 0.25.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 +68 -68
  2. package/dist/index.js +3092 -378
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6901,7 +6901,7 @@ var {
6901
6901
  } = import_index.default;
6902
6902
 
6903
6903
  // src/program.ts
6904
- import { readFileSync as readFileSync28 } from "fs";
6904
+ import { readFileSync as readFileSync31 } from "fs";
6905
6905
 
6906
6906
  // src/utils/config.ts
6907
6907
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -8011,6 +8011,15 @@ function createMockClient() {
8011
8011
  async retryAutomationRun() {
8012
8012
  throw new ApiError(501, "workflows are not available in mock mode");
8013
8013
  },
8014
+ async stopAutomationRun() {
8015
+ throw new ApiError(501, "workflows are not available in mock mode");
8016
+ },
8017
+ async listClientAutomationRuns() {
8018
+ return { items: [] };
8019
+ },
8020
+ async stopClientAutomationRuns() {
8021
+ throw new ApiError(501, "workflows are not available in mock mode");
8022
+ },
8014
8023
  async testRunAutomation() {
8015
8024
  throw new ApiError(501, "workflows are not available in mock mode");
8016
8025
  },
@@ -8612,6 +8621,9 @@ function createMockClient() {
8612
8621
  async previewAudienceSegment() {
8613
8622
  throw new ApiError(501, "audience segments are not available in mock mode");
8614
8623
  },
8624
+ async checkEmailConditions() {
8625
+ throw new ApiError(501, "e-mail conditions are not available in mock mode");
8626
+ },
8615
8627
  async importClients() {
8616
8628
  throw new ApiError(501, "client import is not available in mock mode");
8617
8629
  },
@@ -8648,6 +8660,30 @@ function createMockClient() {
8648
8660
  async rejectManualOrder() {
8649
8661
  throw new ApiError(501, "manual order decisions are not available in mock mode");
8650
8662
  },
8663
+ async markOrderAsTest() {
8664
+ throw new ApiError(501, "order changes are not available in mock mode");
8665
+ },
8666
+ async markOrderRefunded() {
8667
+ throw new ApiError(501, "order changes are not available in mock mode");
8668
+ },
8669
+ async listConsultationBookings() {
8670
+ return { items: [], nextCursor: null, from: "", to: "" };
8671
+ },
8672
+ async listConsultationClientsWithoutBooking() {
8673
+ return { items: [], nextCursor: null };
8674
+ },
8675
+ async listCourseQuestions() {
8676
+ return { questions: [], nextCursor: null };
8677
+ },
8678
+ async replyToCourseQuestion() {
8679
+ throw new ApiError(501, "lesson Q&A is not available in mock mode");
8680
+ },
8681
+ async setCourseQuestionStatus() {
8682
+ throw new ApiError(501, "lesson Q&A is not available in mock mode");
8683
+ },
8684
+ async acceptCourseAnswer() {
8685
+ throw new ApiError(501, "lesson Q&A is not available in mock mode");
8686
+ },
8651
8687
  async listFailedSubscriptionPayments() {
8652
8688
  return { items: [], stripeConnected: false };
8653
8689
  },
@@ -8852,6 +8888,21 @@ function createMockClient() {
8852
8888
  async listCommunityEventAttendees() {
8853
8889
  throw new ApiError(501, "community events are not available in mock mode");
8854
8890
  },
8891
+ async listCommunityJoinRequests() {
8892
+ return { requests: [], pendingCount: 0 };
8893
+ },
8894
+ async approveCommunityJoinRequests() {
8895
+ throw new ApiError(501, "community join requests are not available in mock mode");
8896
+ },
8897
+ async declineCommunityJoinRequests() {
8898
+ throw new ApiError(501, "community join requests are not available in mock mode");
8899
+ },
8900
+ async listCommunityDigestDrafts() {
8901
+ return { digestEnabled: false, current: null, history: [] };
8902
+ },
8903
+ async decideCommunityDigestDraft() {
8904
+ throw new ApiError(501, "the community digest is not available in mock mode");
8905
+ },
8855
8906
  async getCommunityModerationQueue() {
8856
8907
  return { pending: [], resolved: [] };
8857
8908
  },
@@ -8924,6 +8975,36 @@ function createMockClient() {
8924
8975
  async getLiveRoomAttendance() {
8925
8976
  throw new ApiError(501, "webinars are not available in mock mode");
8926
8977
  },
8978
+ async createExternalWebinar() {
8979
+ throw new ApiError(501, "webinars are not available in mock mode");
8980
+ },
8981
+ async setLiveRoomReplay() {
8982
+ throw new ApiError(501, "webinars are not available in mock mode");
8983
+ },
8984
+ async setLiveRoomStudioScene() {
8985
+ throw new ApiError(501, "webinars are not available in mock mode");
8986
+ },
8987
+ async createLiveRoomStudioGuestInvite() {
8988
+ throw new ApiError(501, "webinars are not available in mock mode");
8989
+ },
8990
+ async listLiveRoomStudioGuests() {
8991
+ throw new ApiError(501, "webinars are not available in mock mode");
8992
+ },
8993
+ async revokeLiveRoomStudioGuestInvite() {
8994
+ throw new ApiError(501, "webinars are not available in mock mode");
8995
+ },
8996
+ async listLiveRoomStudioOutputs() {
8997
+ throw new ApiError(501, "webinars are not available in mock mode");
8998
+ },
8999
+ async createLiveRoomStudioOutput() {
9000
+ throw new ApiError(501, "webinars are not available in mock mode");
9001
+ },
9002
+ async updateLiveRoomStudioOutput() {
9003
+ throw new ApiError(501, "webinars are not available in mock mode");
9004
+ },
9005
+ async deleteLiveRoomStudioOutput() {
9006
+ throw new ApiError(501, "webinars are not available in mock mode");
9007
+ },
8927
9008
  async call(method, pathTemplate) {
8928
9009
  throw new ApiError(501, `${method} ${pathTemplate} is not available in mock mode`);
8929
9010
  },
@@ -8984,6 +9065,15 @@ function createMockClient() {
8984
9065
  async createMetaAd() {
8985
9066
  throw new ApiError(501, "Meta Ads are not available in mock mode");
8986
9067
  },
9068
+ async createMetaCampaign() {
9069
+ throw new ApiError(501, "Meta Ads are not available in mock mode");
9070
+ },
9071
+ async createMetaAdSet() {
9072
+ throw new ApiError(501, "Meta Ads are not available in mock mode");
9073
+ },
9074
+ async createMetaAdFromScratch() {
9075
+ throw new ApiError(501, "Meta Ads are not available in mock mode");
9076
+ },
8987
9077
  async updateMetaAd() {
8988
9078
  throw new ApiError(501, "Meta Ads are not available in mock mode");
8989
9079
  },
@@ -9014,6 +9104,9 @@ function createMockClient() {
9014
9104
  async generateFunnelStepPage() {
9015
9105
  throw new ApiError(501, "funnels are not available in mock mode");
9016
9106
  },
9107
+ async addFunnelOfferPage() {
9108
+ throw new ApiError(501, "funnels are not available in mock mode");
9109
+ },
9017
9110
  async listFunnelTemplates() {
9018
9111
  return { templates: [] };
9019
9112
  },
@@ -9044,6 +9137,9 @@ function createMockClient() {
9044
9137
  async getSiteSettings() {
9045
9138
  throw new ApiError(501, "site settings are not available in mock mode");
9046
9139
  },
9140
+ async getSiteCustomScripts() {
9141
+ throw new ApiError(501, "site settings are not available in mock mode");
9142
+ },
9047
9143
  async updateSiteGeneral() {
9048
9144
  throw new ApiError(501, "site settings are not available in mock mode");
9049
9145
  },
@@ -9092,6 +9188,66 @@ function createMockClient() {
9092
9188
  async updateSiteLegalDocuments() {
9093
9189
  throw new ApiError(501, "legal documents are not available in mock mode");
9094
9190
  },
9191
+ async listLibrarySections() {
9192
+ return { sections: [], unassignedProducts: [] };
9193
+ },
9194
+ async replaceLibrarySections() {
9195
+ throw new ApiError(501, "library sections are not available in mock mode");
9196
+ },
9197
+ async createLibrarySection() {
9198
+ throw new ApiError(501, "library sections are not available in mock mode");
9199
+ },
9200
+ async reorderLibrarySections() {
9201
+ throw new ApiError(501, "library sections are not available in mock mode");
9202
+ },
9203
+ async moveLibraryProducts() {
9204
+ throw new ApiError(501, "library sections are not available in mock mode");
9205
+ },
9206
+ async renameLibrarySection() {
9207
+ throw new ApiError(501, "library sections are not available in mock mode");
9208
+ },
9209
+ async deleteLibrarySection() {
9210
+ throw new ApiError(501, "library sections are not available in mock mode");
9211
+ },
9212
+ async listStudioSocialDrafts() {
9213
+ throw new ApiError(501, "Studio drafts are not available in mock mode");
9214
+ },
9215
+ async generateStudioSocialDrafts() {
9216
+ throw new ApiError(501, "Studio drafts are not available in mock mode");
9217
+ },
9218
+ async sendStudioSocialDraftFeedback() {
9219
+ throw new ApiError(501, "Studio drafts are not available in mock mode");
9220
+ },
9221
+ async sendStudioSocialDraftToComposer() {
9222
+ throw new ApiError(501, "Studio drafts are not available in mock mode");
9223
+ },
9224
+ async getSocialWeekReport() {
9225
+ throw new ApiError(501, "social insights are not available in mock mode");
9226
+ },
9227
+ async getSocialPostPerformance() {
9228
+ throw new ApiError(501, "social insights are not available in mock mode");
9229
+ },
9230
+ async setYouTubeVideoVisibility() {
9231
+ throw new ApiError(501, "YouTube visibility is not available in mock mode");
9232
+ },
9233
+ async notifyCheckoutWaitlist() {
9234
+ throw new ApiError(501, "the offer waiting list is not available in mock mode");
9235
+ },
9236
+ async duplicateProduct() {
9237
+ throw new ApiError(501, "product duplicate is not available in mock mode");
9238
+ },
9239
+ async getSenderDomainWarmups() {
9240
+ return { warmups: [] };
9241
+ },
9242
+ async declareSenderDomainWarmed() {
9243
+ throw new ApiError(501, "domain warm-up is not available in mock mode");
9244
+ },
9245
+ async deleteClientEmailDrafts() {
9246
+ throw new ApiError(501, "client e-mails are not available in mock mode");
9247
+ },
9248
+ async resendFormSubmissionRoomLink() {
9249
+ throw new ApiError(501, "form submissions are not available in mock mode");
9250
+ },
9095
9251
  async listClientDevices() {
9096
9252
  throw new ApiError(501, "client devices are not available in mock mode");
9097
9253
  },
@@ -9387,11 +9543,20 @@ function createHttpClient(config) {
9387
9543
  "GET",
9388
9544
  `/automations/${encodeURIComponent(id)}/runs${limit !== void 0 ? `?limit=${limit}` : ""}`
9389
9545
  ),
9390
- setAutomationStatus: (id, status) => request("POST", `/automations/${encodeURIComponent(id)}/status`, { status }),
9546
+ setAutomationStatus: (id, status, heldEvents) => request("POST", `/automations/${encodeURIComponent(id)}/status`, {
9547
+ status,
9548
+ ...heldEvents ? { heldEvents } : {}
9549
+ }),
9391
9550
  retryAutomationRun: (automationId, runId) => request(
9392
9551
  "POST",
9393
9552
  `/automations/${encodeURIComponent(automationId)}/runs/${encodeURIComponent(runId)}/retry`
9394
9553
  ),
9554
+ stopAutomationRun: (automationId, runId) => request(
9555
+ "POST",
9556
+ `/automations/${encodeURIComponent(automationId)}/runs/${encodeURIComponent(runId)}/stop`
9557
+ ),
9558
+ listClientAutomationRuns: (clientId) => request("GET", `/clients/${encodeURIComponent(clientId)}/automation-runs`),
9559
+ stopClientAutomationRuns: (clientId) => request("POST", `/clients/${encodeURIComponent(clientId)}/automation-runs/stop`),
9395
9560
  testRunAutomation: (automationId, input) => request("POST", `/automations/${encodeURIComponent(automationId)}/test-run`, input),
9396
9561
  getCourseContent: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/course`),
9397
9562
  getCourseAnalytics: (productId, options) => {
@@ -9833,7 +9998,12 @@ function createHttpClient(config) {
9833
9998
  createAudienceSegment: (input) => request("POST", "/segments", input),
9834
9999
  updateAudienceSegment: (id, input) => request("PATCH", `/segments/${encodeURIComponent(id)}`, input),
9835
10000
  deleteAudienceSegment: (id) => request("DELETE", `/segments/${encodeURIComponent(id)}`),
9836
- previewAudienceSegment: (conditions) => request("POST", "/segments/preview", { conditions }),
10001
+ previewAudienceSegment: (conditions, options = {}) => request("POST", "/segments/preview", {
10002
+ conditions,
10003
+ ...options.requireSendable ? { requireSendable: true } : {},
10004
+ ...options.requireSmsSendable ? { requireSmsSendable: true } : {}
10005
+ }),
10006
+ checkEmailConditions: (input) => request("POST", "/automations/check-email-conditions", input),
9837
10007
  importClients: (input) => request("POST", "/clients/import", input),
9838
10008
  getClientMergePreview: (survivorClientId, duplicateClientId) => {
9839
10009
  const params = new URLSearchParams();
@@ -9863,6 +10033,29 @@ function createHttpClient(config) {
9863
10033
  // Manual (wire-transfer) order decisions.
9864
10034
  approveManualOrder: (orderId, input) => request("POST", `/orders/${encodeURIComponent(orderId)}/approve-payment`, input),
9865
10035
  rejectManualOrder: (orderId) => request("POST", `/orders/${encodeURIComponent(orderId)}/reject-payment`),
10036
+ markOrderAsTest: (orderId, isTest) => request("POST", `/orders/${encodeURIComponent(orderId)}/mark-test`, { isTest }),
10037
+ markOrderRefunded: (orderId, input) => request("POST", `/orders/${encodeURIComponent(orderId)}/mark-refunded`, input),
10038
+ listConsultationBookings: (options) => request("GET", `/consultations/bookings${toQuery(options)}`),
10039
+ listConsultationClientsWithoutBooking: (options) => request("GET", `/consultations/clients-without-booking${toQuery(options)}`),
10040
+ listCourseQuestions: (productId, options) => request(
10041
+ "GET",
10042
+ `/products/${encodeURIComponent(productId)}/course/questions${toQuery(options)}`
10043
+ ),
10044
+ replyToCourseQuestion: (productId, questionId, content) => request(
10045
+ "POST",
10046
+ `/products/${encodeURIComponent(productId)}/course/questions/${encodeURIComponent(questionId)}/replies`,
10047
+ { content }
10048
+ ),
10049
+ setCourseQuestionStatus: (productId, entryId, status) => request(
10050
+ "PATCH",
10051
+ `/products/${encodeURIComponent(productId)}/course/questions/${encodeURIComponent(entryId)}`,
10052
+ { status }
10053
+ ),
10054
+ acceptCourseAnswer: (productId, questionId, replyId) => request(
10055
+ "POST",
10056
+ `/products/${encodeURIComponent(productId)}/course/questions/${encodeURIComponent(questionId)}/accept`,
10057
+ { replyId }
10058
+ ),
9866
10059
  // Subscriptions: failed payments, manual renewals, ending soon.
9867
10060
  listFailedSubscriptionPayments: () => request("GET", "/subscriptions/failed-payments"),
9868
10061
  retrySubscriptionPayment: (invoiceId) => request("POST", `/subscriptions/failed-payments/${encodeURIComponent(invoiceId)}/retry`),
@@ -10089,6 +10282,23 @@ function createHttpClient(config) {
10089
10282
  "GET",
10090
10283
  `/v1/communities/${encodeURIComponent(communityId)}/events/${encodeURIComponent(eventId)}/attendees`
10091
10284
  ),
10285
+ listCommunityJoinRequests: (communityId) => communityRequest("GET", `/v1/communities/${encodeURIComponent(communityId)}/join-requests`),
10286
+ approveCommunityJoinRequests: (communityId, requestIds) => communityRequest(
10287
+ "POST",
10288
+ `/v1/communities/${encodeURIComponent(communityId)}/join-requests/approve`,
10289
+ { requestIds }
10290
+ ),
10291
+ declineCommunityJoinRequests: (communityId, requestIds) => communityRequest(
10292
+ "POST",
10293
+ `/v1/communities/${encodeURIComponent(communityId)}/join-requests/decline`,
10294
+ { requestIds }
10295
+ ),
10296
+ listCommunityDigestDrafts: (communityId) => communityRequest("GET", `/v1/communities/${encodeURIComponent(communityId)}/digest-drafts`),
10297
+ decideCommunityDigestDraft: (communityId, input) => communityRequest(
10298
+ "POST",
10299
+ `/v1/communities/${encodeURIComponent(communityId)}/digest-drafts/decide`,
10300
+ input
10301
+ ),
10092
10302
  getCommunityModerationQueue: (communityId) => communityRequest(
10093
10303
  "GET",
10094
10304
  `/v1/communities/${encodeURIComponent(communityId)}/moderation/reports`
@@ -10141,8 +10351,28 @@ function createHttpClient(config) {
10141
10351
  "GET",
10142
10352
  `/forms/${encodeURIComponent(id)}/submissions${limit !== void 0 ? `?limit=${limit}` : ""}`
10143
10353
  ),
10144
- // Live rooms live on the Workspace API (api-v1), read only.
10354
+ // Live rooms live on the Workspace API (api-v1): reads, the external webinar and its replay.
10145
10355
  listLiveRooms: () => request("GET", "/live-rooms"),
10356
+ createExternalWebinar: (input) => request("POST", "/live-rooms", input),
10357
+ setLiveRoomReplay: (roomId, mediaId) => request("PUT", `/live-rooms/${encodeURIComponent(roomId)}/replay`, { mediaId }),
10358
+ setLiveRoomStudioScene: (roomId, input) => request("POST", `/live-rooms/${encodeURIComponent(roomId)}/studio/scene`, input),
10359
+ createLiveRoomStudioGuestInvite: (roomId, input) => request("POST", `/live-rooms/${encodeURIComponent(roomId)}/studio/guests`, input),
10360
+ listLiveRoomStudioGuests: (roomId) => request("GET", `/live-rooms/${encodeURIComponent(roomId)}/studio/guests`),
10361
+ revokeLiveRoomStudioGuestInvite: (roomId, guestId) => request(
10362
+ "DELETE",
10363
+ `/live-rooms/${encodeURIComponent(roomId)}/studio/guests/${encodeURIComponent(guestId)}`
10364
+ ),
10365
+ listLiveRoomStudioOutputs: (roomId) => request("GET", `/live-rooms/${encodeURIComponent(roomId)}/studio/outputs`),
10366
+ createLiveRoomStudioOutput: (roomId, input) => request("POST", `/live-rooms/${encodeURIComponent(roomId)}/studio/outputs`, input),
10367
+ updateLiveRoomStudioOutput: (roomId, outputId, input) => request(
10368
+ "PATCH",
10369
+ `/live-rooms/${encodeURIComponent(roomId)}/studio/outputs/${encodeURIComponent(outputId)}`,
10370
+ input
10371
+ ),
10372
+ deleteLiveRoomStudioOutput: (roomId, outputId) => request(
10373
+ "DELETE",
10374
+ `/live-rooms/${encodeURIComponent(roomId)}/studio/outputs/${encodeURIComponent(outputId)}`
10375
+ ),
10146
10376
  getLiveRoomAttendance: (roomId, session) => request(
10147
10377
  "GET",
10148
10378
  `/live-rooms/${encodeURIComponent(roomId)}/attendance${session !== void 0 ? `?session=${encodeURIComponent(session)}` : ""}`
@@ -10181,6 +10411,9 @@ function createHttpClient(config) {
10181
10411
  updateMetaAdSet: (scope, adSetId, input) => request("PATCH", `${metaAdAccountPath(scope)}/ad-sets/${encodeURIComponent(adSetId)}`, input),
10182
10412
  listMetaAds: (scope, query) => request("GET", `${metaAdAccountPath(scope)}/ads${toQuery(query)}`),
10183
10413
  createMetaAd: (scope, input) => request("POST", `${metaAdAccountPath(scope)}/ads`, input),
10414
+ createMetaCampaign: (scope, input) => request("POST", `${metaAdAccountPath(scope)}/campaigns`, input),
10415
+ createMetaAdSet: (scope, input) => request("POST", `${metaAdAccountPath(scope)}/ad-sets`, input),
10416
+ createMetaAdFromScratch: (scope, input) => request("POST", `${metaAdAccountPath(scope)}/ads/from-scratch`, input),
10184
10417
  updateMetaAd: (scope, adId, input) => request("PATCH", `${metaAdAccountPath(scope)}/ads/${encodeURIComponent(adId)}`, input),
10185
10418
  getMetaAdCreative: (scope, adId) => request("GET", `${metaAdAccountPath(scope)}/ads/${encodeURIComponent(adId)}/creative`),
10186
10419
  updateMetaAdCreative: (scope, adId, input) => request(
@@ -10196,6 +10429,7 @@ function createHttpClient(config) {
10196
10429
  updateFunnel: (id, input) => request("PATCH", `/funnels/${encodeURIComponent(id)}`, input),
10197
10430
  deleteFunnel: (id) => request("DELETE", `/funnels/${encodeURIComponent(id)}`),
10198
10431
  generateFunnelStepPage: (id, input) => request("POST", `/funnels/${encodeURIComponent(id)}/generate-step-page`, input),
10432
+ addFunnelOfferPage: (id, input) => request("POST", `/funnels/${encodeURIComponent(id)}/offer-page`, input),
10199
10433
  listFunnelTemplates: () => request("GET", "/funnel-templates"),
10200
10434
  createFunnelFromTemplate: (input) => request("POST", "/funnels/from-template", input),
10201
10435
  createFunnelFromPages: (input) => request("POST", "/funnels/from-pages", input),
@@ -10208,6 +10442,7 @@ function createHttpClient(config) {
10208
10442
  deleteOtoOffer: (id) => request("DELETE", `/oto-offers/${encodeURIComponent(id)}`),
10209
10443
  // Site (platform settings / storefront / legal documents) lives on the Workspace API (api-v1).
10210
10444
  getSiteSettings: () => request("GET", "/site/settings"),
10445
+ getSiteCustomScripts: () => request("GET", "/site/custom-scripts"),
10211
10446
  updateSiteGeneral: (input) => request("PATCH", "/site/settings/general", input),
10212
10447
  updateSiteTheme: (input) => request("PATCH", "/site/settings/theme", input),
10213
10448
  updateSiteBranding: (input) => request("PATCH", "/site/settings/branding", input),
@@ -10227,6 +10462,36 @@ function createHttpClient(config) {
10227
10462
  language ? `/site/legal-documents?language=${encodeURIComponent(language)}` : "/site/legal-documents"
10228
10463
  ),
10229
10464
  updateSiteLegalDocuments: (input) => request("PATCH", "/site/legal-documents", input),
10465
+ listLibrarySections: () => request("GET", "/site/library-sections"),
10466
+ replaceLibrarySections: (sections) => request("PUT", "/site/library-sections", { sections }),
10467
+ createLibrarySection: (input) => request("POST", "/site/library-sections", input),
10468
+ reorderLibrarySections: (sectionIds) => request("POST", "/site/library-sections/reorder", { sectionIds }),
10469
+ moveLibraryProducts: (productIds, sectionId) => request("POST", "/site/library-sections/move-products", { productIds, sectionId }),
10470
+ renameLibrarySection: (sectionId, name) => request("PATCH", `/site/library-sections/${encodeURIComponent(sectionId)}`, { name }),
10471
+ deleteLibrarySection: (sectionId) => request("DELETE", `/site/library-sections/${encodeURIComponent(sectionId)}`),
10472
+ // Studio weekly drafts + post insights (`social-token` routes on api-v1).
10473
+ listStudioSocialDrafts: () => request("GET", "/studio/social-drafts"),
10474
+ generateStudioSocialDrafts: (input) => request("POST", "/studio/social-drafts/generate", input),
10475
+ sendStudioSocialDraftFeedback: (draftId, reason) => request("POST", `/studio/social-drafts/${encodeURIComponent(draftId)}/feedback`, {
10476
+ reason
10477
+ }),
10478
+ sendStudioSocialDraftToComposer: (draftId, input) => request("POST", `/studio/social-drafts/${encodeURIComponent(draftId)}/to-composer`, input),
10479
+ getSocialWeekReport: () => request("GET", "/social/reports/weekly"),
10480
+ getSocialPostPerformance: (postId) => request("GET", `/social/posts/${encodeURIComponent(postId)}/performance`),
10481
+ setYouTubeVideoVisibility: (postId, input) => request("POST", `/social/posts/${encodeURIComponent(postId)}/youtube-visibility`, input),
10482
+ notifyCheckoutWaitlist: (checkoutId, dryRun) => request("POST", `/checkouts/${encodeURIComponent(checkoutId)}/notify-waitlist`, { dryRun }),
10483
+ duplicateProduct: (productId, name) => request(
10484
+ "POST",
10485
+ `/products/${encodeURIComponent(productId)}/duplicate`,
10486
+ name !== void 0 ? { name } : {}
10487
+ ),
10488
+ getSenderDomainWarmups: () => request("GET", "/sender-domain/warmups"),
10489
+ declareSenderDomainWarmed: (domain) => request("POST", `/sender-domain/${encodeURIComponent(domain)}/warmup-declarations`),
10490
+ deleteClientEmailDrafts: (input) => request("POST", "/client-emails/delete-drafts", input),
10491
+ resendFormSubmissionRoomLink: (formId, submissionId) => request(
10492
+ "POST",
10493
+ `/forms/${encodeURIComponent(formId)}/submissions/${encodeURIComponent(submissionId)}/room-link`
10494
+ ),
10230
10495
  // Customer access devices (anti-sharing device slots) — Workspace API.
10231
10496
  listClientDevices: (clientId) => request("GET", `/clients/${encodeURIComponent(clientId)}/devices`),
10232
10497
  revokeClientDevice: (clientId, deviceId) => request(
@@ -10949,7 +11214,7 @@ function registerBlogCommands(program2) {
10949
11214
  "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
11215
  ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").requiredOption(
10951
11216
  "--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).`
11217
+ `When the post goes live: the workspace's local time without an offset ("2026-10-26T08:00", read in the workspace time zone: its own zone from Ustawienia \u2192 Workspace, else Europe/Warsaw for a Polish workspace (PL invoice / pl site), else the owner's account zone, else Europe/Warsaw; with the offset of that date, also across a clock change) or an ISO 8601 date-time with an offset / Z (2026-10-26T08:00:00+01:00). At least a minute and at most a year ahead. null = unschedule (back to draft).`
10953
11218
  ).action(async (id, options, command) => {
10954
11219
  const flags = getGlobalFlags(command);
10955
11220
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -11077,7 +11342,7 @@ function registerSiteRedirectsCommands(program2) {
11077
11342
  emit(flags, result ?? {}, () => printTable(rows, listSiteRedirectsColumns));
11078
11343
  });
11079
11344
  siteRedirects.command("create").summary("Add a site redirect").description(
11080
- "Adds one line at the end of the site's 301 map; live at once. Refused (400 `site-redirects-refused`, `issues[]` = index, from, to, issue) exactly like the dashboard save: a malformed address (`invalid-from` / `invalid-to`), the home page or an address Zanfia owns (`/blog`, `/p/\u2026`, `/app`: `reserved-from`), the member area (`portal-from`), a published page (`page-exists`), a `from` the map already has (`duplicate`), a chain or a loop (`chain`), `/*` in `to` without a folder `from` (`rest-without-folder`), over 500 lines (`too-many-entries`). Returns the line as stored."
11345
+ "Adds one line at the end of the site's 301 map; live at once. Refused (400 `site-redirects-refused`, `issues[]` = index, from, to, issue) exactly like the dashboard save: a malformed address (`invalid-from` / `invalid-to`), the home page or an address Zanfia owns (`/blog`, `/p/\u2026`, `/app`: `reserved-from`), the member area (`portal-from`), a published page (`page-exists`), a `from` the map already has (`duplicate`), a `to` that is another line (`chain`) or the line itself (`loop`), `/*` in `to` without a folder `from` (`rest-without-folder`), over 500 lines (`too-many-entries`). Returns the line as stored."
11081
11346
  ).argument(
11082
11347
  "<from>",
11083
11348
  "The old address on the site: a path (`/o-nas`), a full address on the site (`https://example.com/o-nas/`, stored as its path) or a folder ending with `/*` (`/news/*`, every address under it). Query string and `#\u2026` are dropped; case is kept."
@@ -11338,131 +11603,414 @@ function registerPopupsCommands(program2) {
11338
11603
  });
11339
11604
  }
11340
11605
 
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"
11606
+ // src/commands/_generated/products.ts
11607
+ var listProductBuyersNotificationsColumns = [
11608
+ "id",
11609
+ "title",
11610
+ "status",
11611
+ "createdAt",
11612
+ "confirmedRecipientCount"
11613
+ ];
11614
+ function registerProductsCommands(program2) {
11615
+ const products2 = program2.commands.find((command) => command.name() === "products") ?? program2.command("products");
11616
+ if (!products2.description()) {
11617
+ products2.description(
11618
+ "A product's buyer notifications (\u201EPowiadom kupuj\u0105cych\u201D): list, preview, send"
11348
11619
  );
11349
11620
  }
11350
- const studioVideo = studio.command("video");
11351
- studioVideo.description(
11352
- "Studio videos: suggest clips, write a reel storyboard, voice it, export and download"
11621
+ const productsNotifications = products2.command("notifications");
11622
+ productsNotifications.description(
11623
+ "A product's buyer notifications (\u201EPowiadom kupuj\u0105cych\u201D): list, preview, send"
11353
11624
  );
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."
11625
+ productsNotifications.command("preview").summary("Preview a notification to a product's buyers").description(
11626
+ "Step 1 of \u201EPowiadom kupuj\u0105cych\u201D: how many people a push + portal bell (+ optional e-mail) to everyone with access to ONE product would reach right now, and whether a limit stops it (one send per product per hour, a daily cap per workspace; only a published product). Sends nothing. Show the count to the human and get an explicit yes before notify-product-buyers. Also checks the optional link (own platform only)."
11627
+ ).argument("<productId>", "Product id (`prod_\u2026`) from list-products.").option(
11628
+ "--link <value>",
11629
+ "Optional link the notification opens: a page of the creator's OWN platform, as a path (`/app/courses/{slug}?lesson={lessonId}`) or a URL on the platform domain. Any other host is refused (400 invalid_link). Empty = the product page."
11356
11630
  ).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)."
11631
+ "--send-email",
11632
+ "The e-mail would be ticked: also check the monthly e-mail allowance (`emailSkipped`)."
11633
+ ).option("--no-send-email", "Send `sendEmail: false` explicitly.").action(
11634
+ async (productId, options, command) => {
11635
+ const flags = getGlobalFlags(command);
11636
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11637
+ const params = {
11638
+ productId
11639
+ };
11640
+ const payload = {
11641
+ link: options.link,
11642
+ sendEmail: options.sendEmail
11643
+ };
11644
+ const result = await client.call(
11645
+ "POST",
11646
+ "/products/:productId/buyers-notifications/preview",
11647
+ params,
11648
+ void 0,
11649
+ payload
11650
+ );
11651
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11652
+ }
11653
+ );
11654
+ productsNotifications.command("send").summary("Notify a product's buyers").description(
11655
+ "Step 2 of \u201EPowiadom kupuj\u0105cych\u201D: sends a short message (title + up to 200 characters + an optional link on the own platform) to everyone with access to ONE product right now: a push in the mobile app, a bell item on the portal and, with `sendEmail`, an e-mail. HIGHEST-STAKES ACTION: call preview-product-buyers-notification first, tell the human the recipient count and send only after an explicit yes, passing that count as `confirmedRecipientCount`. Limits: one send per product per hour, a daily cap per workspace (429). The delivery runs in the background: list-product-buyers-notifications shows the counts a little later. It cannot be recalled."
11656
+ ).argument("<productId>", "Product id (`prod_\u2026`) from list-products.").requiredOption(
11657
+ "--title <value>",
11658
+ "The notification title (push title, bell line, e-mail subject). One line."
11659
+ ).requiredOption("--body <value>", "The message, up to 200 characters.").option(
11660
+ "--link <value>",
11661
+ "Optional link the notification opens: a page of the creator's OWN platform, as a path (`/app/courses/{slug}?lesson={lessonId}`) or a URL on the platform domain. Any other host is refused (400 invalid_link). Empty = the product page."
11375
11662
  ).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) => {
11663
+ "--send-email",
11664
+ "Also send it as an e-mail (clients who unsubscribed are skipped). Default: no.",
11665
+ false
11666
+ ).requiredOption(
11667
+ "--confirmed-recipient-count <n>",
11668
+ "The `recipientCount` from preview-product-buyers-notification that the human confirmed. More people with access now = refused (409 recipient_count_changed): preview again.",
11669
+ parseNumberOption("--confirmed-recipient-count", NumberFormat.Integer)
11670
+ ).option(
11671
+ "--send-id <value>",
11672
+ "Idempotency key (8-64 of A-Z a-z 0-9 _ -): a retried call with the same id returns the first send instead of sending again. Generated when omitted."
11673
+ ).option(
11674
+ "--confirm",
11675
+ "required \u2014 acknowledge that you want to notify every buyer of this product now (it cannot be recalled)"
11676
+ ).action(async (productId, options, command) => {
11379
11677
  const flags = getGlobalFlags(command);
11678
+ requireConfirm(
11679
+ options.confirm,
11680
+ "notify every buyer of this product now (it cannot be recalled)"
11681
+ );
11380
11682
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11381
- const params = {};
11683
+ const params = {
11684
+ productId
11685
+ };
11382
11686
  const payload = {
11383
- mediaId,
11384
- language: options.language
11687
+ title: options.title,
11688
+ body: options.body,
11689
+ link: options.link,
11690
+ sendEmail: options.sendEmail,
11691
+ confirmedRecipientCount: options.confirmedRecipientCount,
11692
+ sendId: options.sendId
11385
11693
  };
11386
11694
  const result = await client.call(
11387
11695
  "POST",
11388
- "/studio/videos/clip-suggestions",
11696
+ "/products/:productId/buyers-notifications",
11389
11697
  params,
11390
11698
  void 0,
11391
11699
  payload
11392
11700
  );
11393
- const rows = tableRowsOf(result, "clips");
11394
- emit(flags, result ?? {}, () => printTable(rows, suggestStudioVideoClipsColumns));
11701
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11395
11702
  });
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) => {
11703
+ productsNotifications.command("list").summary("List a product's buyer notifications").description(
11704
+ "The send log of \u201EPowiadom kupuj\u0105cych\u201D for ONE product, newest first: date, text, link, status and what was delivered (recipients, bells, pushes, e-mails, opted out)."
11705
+ ).argument("<productId>", "Product id (`prod_\u2026`) from list-products.").option(
11706
+ "--limit <n>",
11707
+ "Newest first; default 20, max 50.",
11708
+ parseNumberOption("--limit", NumberFormat.Integer)
11709
+ ).action(
11710
+ async (productId, options, command) => {
11711
+ const flags = getGlobalFlags(command);
11712
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11713
+ const params = {
11714
+ productId
11715
+ };
11716
+ const payload = {
11717
+ limit: options.limit
11718
+ };
11719
+ const result = await client.call(
11720
+ "GET",
11721
+ "/products/:productId/buyers-notifications",
11722
+ params,
11723
+ payload,
11724
+ void 0
11725
+ );
11726
+ const rows = tableRowsOf(result, "items");
11727
+ emit(flags, result ?? {}, () => printTable(rows, listProductBuyersNotificationsColumns));
11728
+ }
11729
+ );
11730
+ const productsStock = products2.command("stock", { hidden: true });
11731
+ productsStock.description("A physical product's stock per variant");
11732
+ productsStock.command("get", { hidden: true }).summary("Read a physical product's stock").description(
11733
+ "\u201EWarianty i stan magazynowy\u201D of ONE physical product: per variant (a price of the product's default offer) its stock (the total that may ever sell; null = no limit), the units sold, the units reserved by unpaid orders, what is still available and whether it is personalised. 400 for a digital product, 409 when the product has no offer yet. Read-only."
11734
+ ).argument("<productId>", "The physical product id (`prod_\u2026` from list-products).").action(async (productId, _options, command) => {
11409
11735
  const flags = getGlobalFlags(command);
11410
11736
  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
11737
+ const params = {
11738
+ productId
11417
11739
  };
11418
11740
  const result = await client.call(
11419
- "POST",
11420
- "/studio/videos/storyboards",
11741
+ "GET",
11742
+ "/products/:productId/stock",
11421
11743
  params,
11422
11744
  void 0,
11423
- payload
11745
+ void 0
11424
11746
  );
11425
11747
  emit(flags, result ?? {}, () => printJson(result ?? {}));
11426
11748
  });
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) => {
11749
+ productsStock.command("set", { hidden: true }).summary("Set a physical product's stock").description(
11750
+ "Sets the stock of a physical product's variants, like saving \u201EWarianty i stan magazynowy\u201D: each listed price of the product's default offer is saved with only its stock (`quantityLimit`) changed; label, amount and every other setting stay. `stock` is the TOTAL that may ever sell (sold and reserved units count against it); null = no limit; 0 is refused (Zanfia reads 0 as no limit). Returns the stock after the save. Reversible: set it again."
11751
+ ).argument("<productId>", "The physical product id (`prod_\u2026` from list-products).").requiredOption(
11752
+ "--variants <json|file>",
11753
+ "Only the variants listed change; the rest keep their stock.",
11754
+ parseJsonArrayFileOrInline("--variants")
11755
+ ).option("--confirm", "required \u2014 acknowledge that you want to change this product's stock").action(async (productId, options, command) => {
11445
11756
  const flags = getGlobalFlags(command);
11757
+ requireConfirm(options.confirm, "change this product's stock");
11446
11758
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11447
- const params = {};
11759
+ const params = {
11760
+ productId
11761
+ };
11448
11762
  const payload = {
11449
- scenes: options.scenes,
11450
- voiceId: options.voiceId,
11451
- language: options.language,
11452
- requestId: options.requestId
11763
+ variants: options.variants
11453
11764
  };
11454
11765
  const result = await client.call(
11455
- "POST",
11456
- "/studio/videos/voiceovers",
11766
+ "PATCH",
11767
+ "/products/:productId/stock",
11457
11768
  params,
11458
11769
  void 0,
11459
11770
  payload
11460
11771
  );
11461
11772
  emit(flags, result ?? {}, () => printJson(result ?? {}));
11462
11773
  });
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(
11774
+ }
11775
+
11776
+ // src/commands/_generated/subscriptions.ts
11777
+ var listInstallmentArrearsColumns = [
11778
+ "productName",
11779
+ "clientEmail",
11780
+ "subscriptionStatus",
11781
+ "installmentsPaid",
11782
+ "installmentsTotal",
11783
+ "overdueAmountMinor",
11784
+ "currency"
11785
+ ];
11786
+ function registerSubscriptionsCommands(program2) {
11787
+ const subscriptions = program2.commands.find((command) => command.name() === "subscriptions") ?? program2.command("subscriptions");
11788
+ if (!subscriptions.description()) {
11789
+ subscriptions.description(
11790
+ "Instalment arrears (\u201EZaleg\u0142o\u015Bci w ratach\u201D) and the non-payment setting"
11791
+ );
11792
+ }
11793
+ const subscriptionsArrears = subscriptions.command("arrears");
11794
+ subscriptionsArrears.description(
11795
+ "Instalment arrears (\u201EZaleg\u0142o\u015Bci w ratach\u201D) and the non-payment setting"
11796
+ );
11797
+ subscriptionsArrears.command("setting").summary("What happens when a buyer stops paying instalments").description(
11798
+ "The workspace's choice for an instalment plan Stripe ends before its last instalment (a non-payment): `revoke` ends the access like a plain subscription end, `keep` leaves it and lists the buyer on \u201EZaleg\u0142o\u015Bci\u201D. Default `revoke`. It applies to plans bought since the setting shipped; older plans always keep their access (they show on \u201EZaleg\u0142o\u015Bci\u201D). A plan paid in full is never affected."
11799
+ ).action(async (_options, command) => {
11800
+ const flags = getGlobalFlags(command);
11801
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11802
+ const params = {};
11803
+ const result = await client.call(
11804
+ "GET",
11805
+ "/settings/installment-non-payment",
11806
+ params,
11807
+ void 0,
11808
+ void 0
11809
+ );
11810
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11811
+ });
11812
+ subscriptionsArrears.command("set-setting").summary("Change what happens when a buyer stops paying instalments").description(
11813
+ "Saves the workspace's choice: `revoke` (end the access like a plain subscription end) or `keep` (leave it, list the buyer on \u201EZaleg\u0142o\u015Bci\u201D). It applies the next time Stripe ends an unfinished plan bought since the setting shipped; no access changes now, and plans from before it always keep their access. Answers the setting as saved."
11814
+ ).addOption(
11815
+ new Option(
11816
+ "--action <revoke|keep>",
11817
+ "`revoke` = end the access like a plain subscription end (same e-mails, community / Discord removal); `keep` = leave the access and list the buyer on \u201EZaleg\u0142o\u015Bci\u201D."
11818
+ ).choices(["revoke", "keep"]).makeOptionMandatory()
11819
+ ).option(
11820
+ "--confirm",
11821
+ "required \u2014 acknowledge that you want to change what happens to every buyer who stops paying instalments"
11822
+ ).action(async (options, command) => {
11823
+ const flags = getGlobalFlags(command);
11824
+ requireConfirm(
11825
+ options.confirm,
11826
+ "change what happens to every buyer who stops paying instalments"
11827
+ );
11828
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11829
+ const params = {};
11830
+ const payload = {
11831
+ action: options.action
11832
+ };
11833
+ const result = await client.call(
11834
+ "PATCH",
11835
+ "/settings/installment-non-payment",
11836
+ params,
11837
+ void 0,
11838
+ payload
11839
+ );
11840
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11841
+ });
11842
+ subscriptionsArrears.command("list").summary("List buyers behind on instalments (\u201EZaleg\u0142o\u015Bci\u201D)").description(
11843
+ "Every buyer whose unfinished instalment plan stopped being paid (Stripe past_due / unpaid / canceled), checked live in Stripe first; a buyer who paid outside Stripe (Stripe \u201Emark as paid\u201D, an approved transfer order, a recorded offline payment) is not listed. Per row: what is overdue now and what was never billed (minor units), since when, the instalments paid, whether the access was kept by the setting or because the plan predates it, and whether to check a Fakturownia proforma. Longest overdue first. Read-only."
11844
+ ).option(
11845
+ "--product-id <value>",
11846
+ "One product (`prod_\u2026` from list-products); omitted = every product."
11847
+ ).option(
11848
+ "--include-kept",
11849
+ "Also list the buyers the creator already chose \u201EZostaw\u201D for. Default false."
11850
+ ).option("--no-include-kept", "Send `includeKept: false` explicitly.").action(async (options, command) => {
11851
+ const flags = getGlobalFlags(command);
11852
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11853
+ const params = {};
11854
+ const payload = {
11855
+ productId: options.productId,
11856
+ includeKept: options.includeKept
11857
+ };
11858
+ const result = await client.call("GET", "/installment-arrears", params, payload, void 0);
11859
+ const rows = tableRowsOf(result, "items");
11860
+ emit(flags, result ?? {}, () => printTable(rows, listInstallmentArrearsColumns));
11861
+ });
11862
+ subscriptionsArrears.command("keep").summary("Keep a behind-on-instalments buyer's access (\u201EZostaw\u201D)").description(
11863
+ "\u201EZostaw\u201D on \u201EZaleg\u0142o\u015Bci\u201D: the creator keeps this buyer's access. Only a mark is saved (who, when): the access, Stripe and the buyer are not touched, and the row leaves the default list (`includeKept` shows it). 404 when the buyer is not on the list."
11864
+ ).argument("<productId>", "The row's `productId`.").argument("<clientEmail>", "The row's `clientEmail`.").option(
11865
+ "--confirm",
11866
+ "required \u2014 acknowledge that you want to keep this buyer's access despite the unpaid instalments"
11867
+ ).action(
11868
+ async (productId, clientEmail, options, command) => {
11869
+ const flags = getGlobalFlags(command);
11870
+ requireConfirm(options.confirm, "keep this buyer's access despite the unpaid instalments");
11871
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11872
+ const params = {};
11873
+ const payload = {
11874
+ productId,
11875
+ clientEmail
11876
+ };
11877
+ const result = await client.call(
11878
+ "POST",
11879
+ "/installment-arrears/keep",
11880
+ params,
11881
+ void 0,
11882
+ payload
11883
+ );
11884
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11885
+ }
11886
+ );
11887
+ }
11888
+
11889
+ // src/commands/_generated/studio.ts
11890
+ var suggestStudioVideoClipsColumns = ["startSeconds", "endSeconds", "hook"];
11891
+ function registerStudioCommands(program2) {
11892
+ const studio = program2.commands.find((command) => command.name() === "studio") ?? program2.command("studio");
11893
+ if (!studio.description()) {
11894
+ studio.description(
11895
+ "Studio videos: suggest clips, write a reel storyboard, voice it, export and download"
11896
+ );
11897
+ }
11898
+ const studioVideo = studio.command("video");
11899
+ studioVideo.description(
11900
+ "Studio videos: suggest clips, write a reel storyboard, voice it, export and download"
11901
+ );
11902
+ studioVideo.command("quote").summary("Quote what a Studio video costs").description(
11903
+ "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."
11904
+ ).option(
11905
+ "--length-seconds <n>",
11906
+ "Reel length in seconds of voice: 15, 30 or 45. Default 30.",
11907
+ parseNumberOption("--length-seconds", NumberFormat.Integer)
11908
+ ).action(async (options, command) => {
11909
+ const flags = getGlobalFlags(command);
11910
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11911
+ const params = {};
11912
+ const payload = {
11913
+ lengthSeconds: options.lengthSeconds
11914
+ };
11915
+ const result = await client.call("GET", "/studio/videos/quote", params, payload, void 0);
11916
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11917
+ });
11918
+ studioVideo.command("suggest").summary("Suggest reel clips from a video transcript").description(
11919
+ "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)."
11920
+ ).argument(
11921
+ "<mediaId>",
11922
+ "A transcribed video: a lesson, a LIVE recording or any library video (a media-library id from list-media)."
11923
+ ).option(
11924
+ "--language <value>",
11925
+ "Language of the text, e.g. `pl` or `en`. Omitted: the transcript language, else `pl`."
11926
+ ).action(async (mediaId, options, command) => {
11927
+ const flags = getGlobalFlags(command);
11928
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11929
+ const params = {};
11930
+ const payload = {
11931
+ mediaId,
11932
+ language: options.language
11933
+ };
11934
+ const result = await client.call(
11935
+ "POST",
11936
+ "/studio/videos/clip-suggestions",
11937
+ params,
11938
+ void 0,
11939
+ payload
11940
+ );
11941
+ const rows = tableRowsOf(result, "clips");
11942
+ emit(flags, result ?? {}, () => printTable(rows, suggestStudioVideoClipsColumns));
11943
+ });
11944
+ studioVideo.command("storyboard").summary("Write a reel storyboard (ScriptReel)").description(
11945
+ "\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."
11946
+ ).option("--topic <value>", "What the reel is about; required unless `sourceMediaId`.").option(
11947
+ "--source-media-id <value>",
11948
+ "A transcribed library video (lesson, LIVE) whose transcript grounds the script."
11949
+ ).option(
11950
+ "--length-seconds <n>",
11951
+ "Reel length in seconds of voice: 15, 30 or 45. Default 30.",
11952
+ parseNumberOption("--length-seconds", NumberFormat.Integer)
11953
+ ).option(
11954
+ "--language <value>",
11955
+ "Language of the text, e.g. `pl` or `en`. Omitted: the transcript language, else `pl`."
11956
+ ).action(async (options, command) => {
11957
+ const flags = getGlobalFlags(command);
11958
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11959
+ const params = {};
11960
+ const payload = {
11961
+ topic: options.topic,
11962
+ sourceMediaId: options.sourceMediaId,
11963
+ lengthSeconds: options.lengthSeconds,
11964
+ language: options.language
11965
+ };
11966
+ const result = await client.call(
11967
+ "POST",
11968
+ "/studio/videos/storyboards",
11969
+ params,
11970
+ void 0,
11971
+ payload
11972
+ );
11973
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
11974
+ });
11975
+ studioVideo.command("voiceover").summary("Generate the voice-over of a reel storyboard").description(
11976
+ "\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)."
11977
+ ).requiredOption(
11978
+ "--scenes <json|file>",
11979
+ "The storyboard scenes from write-studio-video-storyboard (edited or not), in order. The voice and the export must use the SAME scenes.",
11980
+ parseJsonArrayFileOrInline("--scenes")
11981
+ ).addOption(
11982
+ new Option(
11983
+ "--voice-id <asia|iwona|pawel|piotr>",
11984
+ "Voice: asia, iwona (female), pawel, piotr (male). Default asia."
11985
+ ).choices(["asia", "iwona", "pawel", "piotr"])
11986
+ ).option(
11987
+ "--language <value>",
11988
+ "Language of the text, e.g. `pl` or `en`. Omitted: the transcript language, else `pl`."
11989
+ ).option(
11990
+ "--request-id <value>",
11991
+ "Idempotency key (a uuid): a retry with the same one returns the same voice, charged once. Omitted = a new voice every call."
11992
+ ).action(async (options, command) => {
11993
+ const flags = getGlobalFlags(command);
11994
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11995
+ const params = {};
11996
+ const payload = {
11997
+ scenes: options.scenes,
11998
+ voiceId: options.voiceId,
11999
+ language: options.language,
12000
+ requestId: options.requestId
12001
+ };
12002
+ const result = await client.call(
12003
+ "POST",
12004
+ "/studio/videos/voiceovers",
12005
+ params,
12006
+ void 0,
12007
+ payload
12008
+ );
12009
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12010
+ });
12011
+ studioVideo.command("export").summary("Export (render) a Studio video").description(
12012
+ "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."
12013
+ ).addOption(
11466
12014
  new Option(
11467
12015
  "--template-id <MomentClip|ScriptReel|Montage|CoursePromo>",
11468
12016
  "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`)."
@@ -11522,109 +12070,449 @@ function registerStudioCommands(program2) {
11522
12070
  );
11523
12071
  emit(flags, result ?? {}, () => printJson(result ?? {}));
11524
12072
  });
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) => {
12073
+ studioVideo.command("status").summary("Read the status of a Studio video export").description(
12074
+ "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."
12075
+ ).argument("<id>", "The job id export-studio-video answered.").action(async (id, _options, command) => {
12076
+ const flags = getGlobalFlags(command);
12077
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12078
+ const params = {
12079
+ jobId: id
12080
+ };
12081
+ const result = await client.call(
12082
+ "GET",
12083
+ "/studio/videos/exports/:jobId",
12084
+ params,
12085
+ void 0,
12086
+ void 0
12087
+ );
12088
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12089
+ });
12090
+ }
12091
+
12092
+ // src/commands/_generated/site.ts
12093
+ var listSiteTemplatePagesColumns = ["pageId", "title", "slug", "issue", "nav"];
12094
+ var getSiteMenuColumns = ["label", "style", "href"];
12095
+ function registerSiteCommands(program2) {
12096
+ const site = program2.commands.find((command) => command.name() === "site") ?? program2.command("site");
12097
+ if (!site.description()) {
12098
+ site.description(
12099
+ "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"
12100
+ );
12101
+ }
12102
+ const siteLayout = site.command("layout");
12103
+ siteLayout.description(
12104
+ "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"
12105
+ );
12106
+ siteLayout.command("get").summary("Get the site layout").description(
12107
+ '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.'
12108
+ ).action(async (_options, command) => {
12109
+ const flags = getGlobalFlags(command);
12110
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12111
+ const params = {};
12112
+ const result = await client.call("GET", "/site/layout", params, void 0, void 0);
12113
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12114
+ });
12115
+ siteLayout.command("pages").summary("List pages that can frame the blog").description(
12116
+ "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`."
12117
+ ).action(async (_options, command) => {
12118
+ const flags = getGlobalFlags(command);
12119
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12120
+ const params = {};
12121
+ const result = await client.call(
12122
+ "GET",
12123
+ "/site/layout/template-pages",
12124
+ params,
12125
+ void 0,
12126
+ void 0
12127
+ );
12128
+ const rows = tableRowsOf(result, "pages");
12129
+ emit(flags, result ?? {}, () => printTable(rows, listSiteTemplatePagesColumns));
12130
+ });
12131
+ siteLayout.command("set").summary("Set what frames the blog").description(
12132
+ "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."
12133
+ ).argument(
12134
+ "<kind>",
12135
+ "`storefront` = the storefront shell; `page` = one of your pages (`pageId`)."
12136
+ ).option(
12137
+ "--page <value>",
12138
+ "With `kind: page`: the page that frames the blog (from list-site-template-pages)."
12139
+ ).action(async (kind, options, command) => {
12140
+ const flags = getGlobalFlags(command);
12141
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12142
+ const params = {};
12143
+ const payload = {
12144
+ kind,
12145
+ pageId: options.page
12146
+ };
12147
+ const result = await client.call("PUT", "/site/layout/template", params, void 0, payload);
12148
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12149
+ });
12150
+ const siteMenu = site.command("menu");
12151
+ siteMenu.description("The site's one shared menu (Your site \u2192 Look and menu): up to 8 entries");
12152
+ siteMenu.command("get").summary("Get the shared site menu").description(
12153
+ "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)."
12154
+ ).action(async (_options, command) => {
12155
+ const flags = getGlobalFlags(command);
12156
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12157
+ const params = {};
12158
+ const result = await client.call("GET", "/site/menu", params, void 0, void 0);
12159
+ const rows = tableRowsOf(result, "menu");
12160
+ emit(flags, result ?? {}, () => printTable(rows, getSiteMenuColumns));
12161
+ });
12162
+ siteMenu.command("set").summary("Set the shared site menu").description(
12163
+ '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.'
12164
+ ).requiredOption(
12165
+ "--menu <json|file>",
12166
+ "EVERY entry of the new menu, in order (up to 8), as a JSON array; an empty list turns the shared menu off.",
12167
+ parseJsonArrayFileOrInline("--menu")
12168
+ ).action(async (options, command) => {
12169
+ const flags = getGlobalFlags(command);
12170
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12171
+ const params = {};
12172
+ const payload = {
12173
+ menu: options.menu
12174
+ };
12175
+ const result = await client.call("PUT", "/site/menu", params, void 0, payload);
12176
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12177
+ });
12178
+ }
12179
+
12180
+ // src/commands/_generated/chats.ts
12181
+ function registerChatsCommands(program2) {
12182
+ const chats = program2.command("chats", { hidden: true });
12183
+ chats.description("Czaty: the assistant and hired agents");
12184
+ chats.command("list", { hidden: true }).summary("List chats with unread counts").description(
12185
+ "The caller's chats (Czaty): the creator assistant's main conversation with its last line and unread count (messages it wrote first, e.g. the morning summary), the older assistant topics, and the hired desk agents (Copywriter, Analyst, Marketer, Finance) with their last line and unread reports. Agents only where the agents early access is on. Behind the early_access_chat_home launch flag (403 feature_not_available otherwise)."
12186
+ ).action(async (_options, command) => {
12187
+ const flags = getGlobalFlags(command);
12188
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12189
+ const params = {};
12190
+ const result = await client.call("GET", "/chat-home", params, void 0, void 0);
12191
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12192
+ });
12193
+ }
12194
+
12195
+ // src/commands/_generated/orders.ts
12196
+ function registerOrdersCommands(program2) {
12197
+ const orders2 = program2.commands.find((command) => command.name() === "orders") ?? program2.command("orders", { hidden: true });
12198
+ if (!orders2.description()) {
12199
+ orders2.description("Physical orders to ship (\u201EDo wys\u0142ania\u201D): list, mark shipped / delivered");
12200
+ }
12201
+ const ordersFulfilment = orders2.command("fulfilment", { hidden: true });
12202
+ ordersFulfilment.description(
12203
+ "Physical orders to ship (\u201EDo wys\u0142ania\u201D): list, mark shipped / delivered"
12204
+ );
12205
+ ordersFulfilment.command("list", { hidden: true }).summary("List physical orders to ship (\u201EDo wys\u0142ania\u201D)").description(
12206
+ "The \u201EDo wys\u0142ania\u201D queue of paid physical orders by shipping status, newest first, with what packing needs per row: the variant, quantity, personalisation, order bumps, the buyer, the delivery (InPost locker code or courier address, phone, parcel size), the shipping status with carrier and tracking number, the charged total, and the \u201ESprzedane ponad stan\u201D flag. `unfulfilledCount` is the badge. Test purchases are never listed. Read-only."
12207
+ ).addOption(
12208
+ new Option(
12209
+ "--status <unfulfilled|shipped|delivered|returned|cancelled>",
12210
+ "The tab: `unfulfilled` (\u201EDo wys\u0142ania\u201D, the default), `shipped` (\u201EWys\u0142ane\u201D), `delivered` (\u201EDor\u0119czone\u201D); also `returned` and `cancelled`."
12211
+ ).choices(["unfulfilled", "shipped", "delivered", "returned", "cancelled"])
12212
+ ).option("--cursor <value>", "`nextCursor` of the previous page; omit for the first page.").option(
12213
+ "--limit <n>",
12214
+ "Rows per page, newest first (default 50, max 200).",
12215
+ parseNumberOption("--limit", NumberFormat.Integer)
12216
+ ).action(async (options, command) => {
12217
+ const flags = getGlobalFlags(command);
12218
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12219
+ const params = {};
12220
+ const payload = {
12221
+ status: options.status,
12222
+ cursor: options.cursor,
12223
+ limit: options.limit
12224
+ };
12225
+ const result = await client.call("GET", "/orders/to-ship", params, payload, void 0);
12226
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12227
+ });
12228
+ ordersFulfilment.command("ship", { hidden: true }).summary("Mark a physical order as shipped").description(
12229
+ "\u201EOznacz jako wys\u0142ane\u201D for ONE paid physical order: records the carrier, the tracking number and link and the shipping date, and by default e-mails the buyer \u201ETwoja paczka jest w drodze\u201D with the tracking link (once per tracking number; `notifyBuyer: false` records it without the e-mail). The first marking fires the order-shipped automations. Marking a shipped order again corrects the carrier or number and keeps the first shipping date. 409 for an unpaid, delivered, returned or cancelled order, 400 for a digital one. The e-mail cannot be recalled: confirm the order and the number first."
12230
+ ).argument("<orderId>", "The order id (`orderId` from list-orders-to-ship or list-orders).").addOption(
12231
+ new Option(
12232
+ "--carrier <inpost|poczta_polska|dpd|dhl|gls|orlen_paczka|other>",
12233
+ "inpost | poczta_polska | dpd | dhl | gls | orlen_paczka | other. InPost gets a tracking link built from the number; other carriers only with `trackingUrl`."
12234
+ ).choices(["inpost", "poczta_polska", "dpd", "dhl", "gls", "orlen_paczka", "other"]).makeOptionMandatory()
12235
+ ).option(
12236
+ "--tracking-number <value>",
12237
+ "The parcel number. Omit for a shipment without one (e.g. a letter)."
12238
+ ).option(
12239
+ "--tracking-url <value>",
12240
+ "A tracking link (http/https). Omit for InPost: it is built from the number. Other carriers without one show the number only."
12241
+ ).option(
12242
+ "--notify-buyer",
12243
+ "E-mail the buyer \u201ETwoja paczka jest w drodze\u201D with the tracking link (default true). Sent once per tracking number: re-marking with the same number sends nothing, a corrected number e-mails again."
12244
+ ).option("--no-notify-buyer", "Send `notifyBuyer: false` explicitly.").option("--confirm", "required \u2014 acknowledge that you want to mark this order as shipped").action(async (orderId, options, command) => {
12245
+ const flags = getGlobalFlags(command);
12246
+ requireConfirm(options.confirm, "mark this order as shipped");
12247
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12248
+ const params = {
12249
+ orderId
12250
+ };
12251
+ const payload = {
12252
+ carrier: options.carrier,
12253
+ trackingNumber: options.trackingNumber,
12254
+ trackingUrl: options.trackingUrl,
12255
+ notifyBuyer: options.notifyBuyer
12256
+ };
12257
+ const result = await client.call(
12258
+ "POST",
12259
+ "/orders/:orderId/mark-shipped",
12260
+ params,
12261
+ void 0,
12262
+ payload
12263
+ );
12264
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12265
+ });
12266
+ ordersFulfilment.command("deliver", { hidden: true }).summary("Mark a physical order as delivered").description(
12267
+ "\u201EOznacz jako dor\u0119czone\u201D (also \u201Eodebrane\u201D for a personal pickup) for ONE paid physical order, from shipped or straight from unfulfilled (a hand delivery). Records the delivery date: the 14-day withdrawal period runs from it. No e-mail goes out. An already delivered order is returned unchanged. 409 for an unpaid, returned or cancelled order, 400 for a digital one."
12268
+ ).argument("<orderId>", "The order id (`orderId` from list-orders-to-ship or list-orders).").option("--confirm", "required \u2014 acknowledge that you want to mark this order as delivered").action(async (orderId, options, command) => {
12269
+ const flags = getGlobalFlags(command);
12270
+ requireConfirm(options.confirm, "mark this order as delivered");
12271
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12272
+ const params = {
12273
+ orderId
12274
+ };
12275
+ const result = await client.call(
12276
+ "POST",
12277
+ "/orders/:orderId/mark-delivered",
12278
+ params,
12279
+ void 0,
12280
+ void 0
12281
+ );
12282
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12283
+ });
12284
+ }
12285
+
12286
+ // src/commands/_generated/tax.ts
12287
+ function registerTaxCommands(program2) {
12288
+ const tax = program2.command("tax");
12289
+ tax.description(
12290
+ "Stripe Tax settings (on / off, inclusive / exclusive prices, EU companies pay net)"
12291
+ );
12292
+ tax.command("get").summary("Read the Stripe Tax settings").description(
12293
+ "The workspace's Stripe Tax settings, as on Integracje \u2192 P\u0142atno\u015Bci \u2192 Stripe: whether Zanfia calculates tax with Stripe Tax (`enabled`), the default price mode while on (`behavior`: inclusive / exclusive), \u201EEU companies pay the net price\u201D (`euCompaniesPayNet`), and Stripe's own Tax state on the connected Stripe account (`accountStatus`, read live from Stripe). Read-only."
12294
+ ).action(async (_options, command) => {
12295
+ const flags = getGlobalFlags(command);
12296
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12297
+ const params = {};
12298
+ const result = await client.call(
12299
+ "GET",
12300
+ "/integrations/payments/stripe/tax",
12301
+ params,
12302
+ void 0,
12303
+ void 0
12304
+ );
12305
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12306
+ });
12307
+ tax.command("set").summary("Change the Stripe Tax settings").description(
12308
+ "Changes the workspace's Stripe Tax settings like the switches on Integracje \u2192 P\u0142atno\u015Bci \u2192 Stripe; only the fields sent change. Turning it on needs a connected Stripe account with Stripe Tax active in Stripe itself (409 `stripe_tax_not_active_on_account` otherwise, with the Stripe settings link) and sets the default mode to `inclusive` unless `behavior` is sent; turning it off is always allowed. `behavior` and `euCompaniesPayNet` exist only while it is on (409 `stripe_tax_off`). It applies to the next Stripe checkouts and to the next renewals of running subscriptions (turning it off stops tax on renewals; `euCompaniesPayNet` changes what EU-company subscribers pay at renewal); paid orders don't change. Answers the settings as saved. Reversible: change them again."
12309
+ ).option(
12310
+ "--enabled",
12311
+ "true = turn Stripe Tax on (needs Stripe Tax active on the connected Stripe account; the default mode becomes `inclusive` unless `behavior` is sent too); false = turn it off (always allowed; clears the mode)."
12312
+ ).option("--no-enabled", "Send `enabled: false` explicitly.").addOption(
12313
+ new Option(
12314
+ "--behavior <inclusive|exclusive>",
12315
+ "The default price mode while on: `inclusive` (gross prices) or `exclusive` (net prices, tax added at checkout). Only while Stripe Tax is on."
12316
+ ).choices(["inclusive", "exclusive"])
12317
+ ).option(
12318
+ "--eu-companies-pay-net",
12319
+ "\u201EFirmy z UE p\u0142ac\u0105 cen\u0119 netto\u201D (KI#1720): a company from another EU country with a valid VAT number pays an inclusive price without the VAT. Only while Stripe Tax is on."
12320
+ ).option("--no-eu-companies-pay-net", "Send `euCompaniesPayNet: false` explicitly.").action(async (options, command) => {
12321
+ const flags = getGlobalFlags(command);
12322
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12323
+ const params = {};
12324
+ const payload = {
12325
+ enabled: options.enabled,
12326
+ behavior: options.behavior,
12327
+ euCompaniesPayNet: options.euCompaniesPayNet
12328
+ };
12329
+ const result = await client.call(
12330
+ "PATCH",
12331
+ "/integrations/payments/stripe/tax",
12332
+ params,
12333
+ void 0,
12334
+ payload
12335
+ );
12336
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12337
+ });
12338
+ }
12339
+
12340
+ // src/commands/_generated/social.ts
12341
+ function registerSocialCommands(program2) {
12342
+ const social = program2.commands.find((command) => command.name() === "social") ?? program2.command("social", { hidden: true });
12343
+ if (!social.description()) {
12344
+ social.description("Automatic messages after an Instagram comment (keyword \u2192 private message)");
12345
+ }
12346
+ const socialCommentDm = social.command("comment-dm", { hidden: true });
12347
+ socialCommentDm.description(
12348
+ "Automatic messages after an Instagram comment (keyword \u2192 private message)"
12349
+ );
12350
+ socialCommentDm.command("list", { hidden: true }).summary("List the automatic messages after a comment").description(
12351
+ "The workspace\u2019s \u201EAutomatyczna wiadomo\u015B\u0107 po komentarzu\u201D rules on Instagram, newest first: the account, the post (or every post of the account), the keywords, the message, the link as sent (`taggedLink`, with UTM tags), the public reply, `status` (active / paused), `ended`, and the results so far in `counts` (matched comments, `sent` and `failed` private messages) with the latest failure. Read-only. 403 `feature_not_available` while the workspace has no early access."
12352
+ ).option(
12353
+ "--account <value>",
12354
+ "Only this Instagram account's rules: its id or its target from list-social-accounts. Omitted = every account."
12355
+ ).action(async (options, command) => {
12356
+ const flags = getGlobalFlags(command);
12357
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12358
+ const params = {};
12359
+ const payload = {
12360
+ accountId: options.account
12361
+ };
12362
+ const result = await client.call(
12363
+ "GET",
12364
+ "/social/instagram/comment-dm-rules",
12365
+ params,
12366
+ payload,
12367
+ void 0
12368
+ );
12369
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12370
+ });
12371
+ socialCommentDm.command("create", { hidden: true }).summary("Create an automatic message after a comment").description(
12372
+ "Creates a \u201EAutomatyczna wiadomo\u015B\u0107 po komentarzu\u201D rule on Instagram: when someone comments one of the keywords under the post (or any post of the account), Instagram sends them ONE private message with the text and the link (UTM-tagged), optionally with a public reply under the comment. Each person gets it once per rule; comments older than 7 days get nothing. Same rules as the panel: 400 `invalid` (with `problem`), 404 `not_connected`, 409 `reconnect_required` (the account lacks the messages / comments permission), 409 `too_many_rules` (50 per workspace), 403 `feature_not_available` without the early access. `commentsSubscribed: false` = saved but Instagram refused the comment notifications (reconnect the account). Pause it with pause-instagram-comment-dm-rule."
12373
+ ).option(
12374
+ "--account <value>",
12375
+ "The Instagram account: its id (`accountId` of a rule), or the `target` of an Instagram account from list-social-accounts (`instagram` = the main account, `instagram:{id}`). Omitted = the main connected Instagram account."
12376
+ ).addOption(
12377
+ new Option(
12378
+ "--scope <post|account>",
12379
+ "`post` = comments under one post (send `mediaId` of a published post from list-instagram-media, or `socialPostId` of a composer post not published yet); `account` = comments under any post of the account."
12380
+ ).choices(["post", "account"]).makeOptionMandatory()
12381
+ ).option("--media-id <value>", "`post` scope: the published post\u2019s media id.").option(
12382
+ "--social-post-id <value>",
12383
+ "`post` scope: a composer post not live yet; the rule waits for its publishing."
12384
+ ).option("--media-permalink <value>", "The post\u2019s Instagram link, shown on the rule.").option("--media-caption <value>", "The post\u2019s caption, shown on the rule.").requiredOption(
12385
+ "--keywords <a,b,...>",
12386
+ "Words or short phrases that trigger the message (1-10, each up to 40 characters). A comment matches when it contains one as whole words; case and Polish letters do not matter (\u201EPobierz!\u201D and \u201Epobierz\u201D match `pobierz`).",
12387
+ parseCommaList("--keywords")
12388
+ ).requiredOption(
12389
+ "--text <value>",
12390
+ "The private message sent once to each person who comments a keyword. With the link it must fit Instagram\u2019s 1000 bytes."
12391
+ ).option(
12392
+ "--link <value>",
12393
+ "Optional full http(s) link added to the message; Zanfia adds UTM tags (utm_source=instagram, utm_medium=comment_dm, utm_campaign=<first keyword>). null removes it."
12394
+ ).option(
12395
+ "--public-reply <json|file>",
12396
+ "Optional public reply under the matching comment; off when omitted.",
12397
+ parseJsonObjectFileOrInline("--public-reply")
12398
+ ).addOption(
12399
+ new Option(
12400
+ "--status <active|paused>",
12401
+ "Omitted = `active` (answers comments at once)."
12402
+ ).choices(["active", "paused"])
12403
+ ).option(
12404
+ "--ends-at <value>",
12405
+ "Optional ISO 8601 time after which the rule answers nothing; null = no end."
12406
+ ).action(async (options, command) => {
12407
+ const flags = getGlobalFlags(command);
12408
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12409
+ const params = {};
12410
+ const payload = {
12411
+ accountId: options.account,
12412
+ scope: options.scope,
12413
+ mediaId: options.mediaId,
12414
+ socialPostId: options.socialPostId,
12415
+ mediaPermalink: options.mediaPermalink,
12416
+ mediaCaption: options.mediaCaption,
12417
+ keywords: options.keywords,
12418
+ dmText: options.text,
12419
+ link: options.link,
12420
+ publicReply: options.publicReply,
12421
+ status: options.status,
12422
+ endsAt: options.endsAt
12423
+ };
12424
+ const result = await client.call(
12425
+ "POST",
12426
+ "/social/instagram/comment-dm-rules",
12427
+ params,
12428
+ void 0,
12429
+ payload
12430
+ );
12431
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12432
+ });
12433
+ socialCommentDm.command("update", { hidden: true }).summary("Change or resume an automatic message after a comment").description(
12434
+ "Changes a \u201EAutomatyczna wiadomo\u015B\u0107 po komentarzu\u201D rule like the panel\u2019s edit: only the fields sent change (keywords, message, link, public reply, end date) and `status` pauses (`paused`) or resumes (`active`) it. The account and the post never change: a new post is a new rule. People who already got the message do not get it again. Same validation as a create (400 `invalid` with `problem`); 404 `rule_not_found`. Reversible: change it again."
12435
+ ).argument("<id>", "The rule id (`id` from list-instagram-comment-dm-rules).").option(
12436
+ "--keywords <a,b,...>",
12437
+ "The full new keyword list (replaces the old one).",
12438
+ parseCommaList("--keywords")
12439
+ ).option(
12440
+ "--text <value>",
12441
+ "The private message sent once to each person who comments a keyword. With the link it must fit Instagram\u2019s 1000 bytes."
12442
+ ).option(
12443
+ "--link <value>",
12444
+ "Optional full http(s) link added to the message; Zanfia adds UTM tags (utm_source=instagram, utm_medium=comment_dm, utm_campaign=<first keyword>). null removes it."
12445
+ ).option(
12446
+ "--public-reply <json|file>",
12447
+ "Optional public reply under the matching comment; off when omitted.",
12448
+ parseJsonObjectFileOrInline("--public-reply")
12449
+ ).addOption(
12450
+ new Option(
12451
+ "--status <active|paused>",
12452
+ "`active` resumes the rule, `paused` pauses it."
12453
+ ).choices(["active", "paused"])
12454
+ ).option(
12455
+ "--ends-at <value>",
12456
+ "Optional ISO 8601 time after which the rule answers nothing; null = no end."
12457
+ ).action(async (id, options, command) => {
12458
+ const flags = getGlobalFlags(command);
12459
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12460
+ const params = {
12461
+ ruleId: id
12462
+ };
12463
+ const payload = {
12464
+ keywords: options.keywords,
12465
+ dmText: options.text,
12466
+ link: options.link,
12467
+ publicReply: options.publicReply,
12468
+ status: options.status,
12469
+ endsAt: options.endsAt
12470
+ };
12471
+ const result = await client.call(
12472
+ "PATCH",
12473
+ "/social/instagram/comment-dm-rules/:ruleId",
12474
+ params,
12475
+ void 0,
12476
+ payload
12477
+ );
12478
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
12479
+ });
12480
+ socialCommentDm.command("pause", { hidden: true }).summary("Pause an automatic message after a comment").description(
12481
+ "Pauses a \u201EAutomatyczna wiadomo\u015B\u0107 po komentarzu\u201D rule (the panel\u2019s \u201EWstrzymaj\u201D): it answers no new comment until resumed (update-instagram-comment-dm-rule with `status: active`). Its settings and results stay. 404 `rule_not_found`."
12482
+ ).argument("<id>", "The rule id (`id` from list-instagram-comment-dm-rules).").action(async (id, _options, command) => {
11528
12483
  const flags = getGlobalFlags(command);
11529
12484
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11530
12485
  const params = {
11531
- jobId: id
12486
+ ruleId: id
11532
12487
  };
11533
12488
  const result = await client.call(
11534
- "GET",
11535
- "/studio/videos/exports/:jobId",
12489
+ "POST",
12490
+ "/social/instagram/comment-dm-rules/:ruleId/pause",
11536
12491
  params,
11537
12492
  void 0,
11538
12493
  void 0
11539
12494
  );
11540
12495
  emit(flags, result ?? {}, () => printJson(result ?? {}));
11541
12496
  });
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) => {
12497
+ socialCommentDm.command("delete", { hidden: true }).summary("Delete an automatic message after a comment").description(
12498
+ "Deletes a \u201EAutomatyczna wiadomo\u015B\u0107 po komentarzu\u201D rule for good, with its results and the list of people who got the message. No undo: prefer pause-instagram-comment-dm-rule. 404 `rule_not_found`."
12499
+ ).argument("<id>", "The rule id (`id` from list-instagram-comment-dm-rules).").option(
12500
+ "--confirm",
12501
+ "required \u2014 acknowledge that you want to delete the rule and its results for good"
12502
+ ).action(async (id, options, command) => {
11570
12503
  const flags = getGlobalFlags(command);
12504
+ requireConfirm(options.confirm, "delete the rule and its results for good");
11571
12505
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11572
- const params = {};
12506
+ const params = {
12507
+ ruleId: id
12508
+ };
11573
12509
  const result = await client.call(
11574
- "GET",
11575
- "/site/layout/template-pages",
12510
+ "DELETE",
12511
+ "/social/instagram/comment-dm-rules/:ruleId",
11576
12512
  params,
11577
12513
  void 0,
11578
12514
  void 0
11579
12515
  );
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
12516
  emit(flags, result ?? {}, () => printJson(result ?? {}));
11629
12517
  });
11630
12518
  }
@@ -11635,11 +12523,18 @@ function registerGeneratedCommands(program2) {
11635
12523
  registerBlogCommands(program2);
11636
12524
  registerSiteRedirectsCommands(program2);
11637
12525
  registerPopupsCommands(program2);
12526
+ registerProductsCommands(program2);
12527
+ registerSubscriptionsCommands(program2);
11638
12528
  registerStudioCommands(program2);
11639
12529
  registerSiteCommands(program2);
12530
+ registerChatsCommands(program2);
12531
+ registerOrdersCommands(program2);
12532
+ registerTaxCommands(program2);
12533
+ registerSocialCommands(program2);
11640
12534
  }
11641
12535
 
11642
12536
  // src/commands/ads/index.ts
12537
+ import { randomUUID } from "crypto";
11643
12538
  import { readFileSync as readFileSync3 } from "fs";
11644
12539
  var SETTABLE_STATUSES = ["ACTIVE", "PAUSED"];
11645
12540
  var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
@@ -11738,6 +12633,74 @@ function parseUpdateCreativeInput(value) {
11738
12633
  }
11739
12634
  return parsed;
11740
12635
  }
12636
+ function parseCreateJson(value, allowed, required) {
12637
+ const parsed = parseJsonObjectFileOrInline2(value, "--json");
12638
+ const unknownKeys = Object.keys(parsed).filter((key) => !allowed.includes(key));
12639
+ if (unknownKeys.length > 0) {
12640
+ throw new Error(`--json accepts only ${allowed.join(", ")} (got: ${unknownKeys.join(", ")}).`);
12641
+ }
12642
+ const missing = required.filter((key) => parsed[key] === void 0);
12643
+ if (missing.length > 0) {
12644
+ throw new Error(`--json needs ${missing.join(", ")}.`);
12645
+ }
12646
+ return parsed;
12647
+ }
12648
+ var CREATE_CAMPAIGN_KEYS = [
12649
+ "name",
12650
+ "objective",
12651
+ "specialAdCategories",
12652
+ "dailyBudgetMinor",
12653
+ "lifetimeBudgetMinor",
12654
+ "endTime"
12655
+ ];
12656
+ var CREATE_AD_SET_KEYS = [
12657
+ "campaignId",
12658
+ "name",
12659
+ "dailyBudgetMinor",
12660
+ "lifetimeBudgetMinor",
12661
+ "startTime",
12662
+ "endTime",
12663
+ "targeting",
12664
+ "publisherPlatforms",
12665
+ "datasetId"
12666
+ ];
12667
+ var CREATE_AD_FROM_SCRATCH_KEYS = [
12668
+ "adSetId",
12669
+ "name",
12670
+ "pageId",
12671
+ "instagramUserId",
12672
+ "imageUrl",
12673
+ "message",
12674
+ "headline",
12675
+ "description",
12676
+ "callToActionType",
12677
+ "link",
12678
+ "urlTags"
12679
+ ];
12680
+ function guardFieldsOf(options, what) {
12681
+ if (!options.dryRun) requireConfirm2(options.confirm, what);
12682
+ const idempotencyKey = options.idempotencyKey ?? randomUUID();
12683
+ if (!options.idempotencyKey && !options.dryRun) {
12684
+ process.stderr.write(
12685
+ `Idempotency key: ${idempotencyKey} (re-run with --idempotency-key ${idempotencyKey} to retry safely)
12686
+ `
12687
+ );
12688
+ }
12689
+ return { idempotencyKey, ...options.dryRun ? { dryRun: true } : {} };
12690
+ }
12691
+ function printCreatePreview(result, nextStep) {
12692
+ const { preview: preview2 } = result;
12693
+ const lead = result.dryRun ? "Dry run, nothing created." : result.replayed ? "Already created with this idempotency key (nothing new on Meta)." : "Created (PAUSED).";
12694
+ process.stdout.write(`${lead}
12695
+ ${preview2.summary}
12696
+ `);
12697
+ process.stdout.write(
12698
+ `Daily ceiling: ${formatMinor(preview2.ceiling.dailyCeilingMinor, preview2.budget.currency)}, used in the last 24 h: ${formatMinor(preview2.ceiling.usedLast24hMinor, preview2.budget.currency)}, left after this: ${formatMinor(preview2.ceiling.remainingAfterMinor, preview2.budget.currency)}.
12699
+ `
12700
+ );
12701
+ if (!result.dryRun) process.stdout.write(`${nextStep}
12702
+ `);
12703
+ }
11741
12704
  function requireConfirm2(confirm, what) {
11742
12705
  if (!confirm) {
11743
12706
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
@@ -12127,6 +13090,33 @@ Daily (${result.daily.length})
12127
13090
  });
12128
13091
  }
12129
13092
  );
13093
+ campaign.command("create").description(
13094
+ "Create a NEW campaign (OUTCOME_TRAFFIC | OUTCOME_SALES | OUTCOME_LEADS), always PAUSED. Optional DAILY campaign budget in minor units (a lifetime campaign budget is not supported yet: put lifetime budgets on the ad sets). Try --dry-run first; a real create requires --confirm."
13095
+ ).argument(...CONNECTION_ARG).argument(...AD_ACCOUNT_ARG).requiredOption(
13096
+ "--json <fileOrJson>",
13097
+ `name, objective (required); specialAdCategories, dailyBudgetMinor (optional) ${JSON_FLAG_HINT}`
13098
+ ).option("--dry-run", "preview only: what would be created, the budget and the ceiling left").option("--idempotency-key <key>", "repeat a key to retry safely (default: a new UUID)").option("--confirm", "required for a real create").action(
13099
+ async (connectionId, adAccountId, options, command) => {
13100
+ const flags = getGlobalFlags(command);
13101
+ const input = parseCreateJson(options.json, CREATE_CAMPAIGN_KEYS, [
13102
+ "name",
13103
+ "objective"
13104
+ ]);
13105
+ const guard = guardFieldsOf(options, `create campaign "${input.name}"`);
13106
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
13107
+ const result = await client.createMetaCampaign(scopeOf(connectionId, adAccountId), {
13108
+ ...input,
13109
+ ...guard
13110
+ });
13111
+ emit(flags, result, () => {
13112
+ printCreatePreview(
13113
+ result,
13114
+ `Next: \`ads ad-set create ${connectionId} ${adAccountId} --json '{"campaignId":"${result.campaign?.id ?? "\u2026"}",\u2026}'\`.`
13115
+ );
13116
+ if (result.campaign) printCampaignTable([result.campaign]);
13117
+ });
13118
+ }
13119
+ );
12130
13120
  ads.command("ad-sets").description("List ad sets of an ad account (budgets, schedule, effective status)").argument(...CONNECTION_ARG).argument(...AD_ACCOUNT_ARG).option("--campaign <campaignId>", "only ad sets of this campaign").option("--include-archived", "include ARCHIVED / DELETED ad sets").action(
12131
13121
  async (connectionId, adAccountId, options, command) => {
12132
13122
  const flags = getGlobalFlags(command);
@@ -12147,6 +13137,34 @@ Daily (${result.daily.length})
12147
13137
  emit(flags, result, () => printAdSet(result.adSet));
12148
13138
  }
12149
13139
  );
13140
+ adSet.command("create").description(
13141
+ "Create a NEW ad set under a campaign, always PAUSED: budget (when the campaign has none), schedule, targeting (countries, age, gender, audience ids), placements, pixel. Try --dry-run first; a real create requires --confirm."
13142
+ ).argument(...CONNECTION_ARG).argument(...AD_ACCOUNT_ARG).requiredOption(
13143
+ "--json <fileOrJson>",
13144
+ `campaignId, name, targeting { countries, ageMin, ageMax, gender, customAudienceIds, excludedCustomAudienceIds, advantageAudience } (required: campaignId, name, targeting.countries); dailyBudgetMinor | lifetimeBudgetMinor + endTime, startTime, publisherPlatforms, datasetId ${JSON_FLAG_HINT}`
13145
+ ).option("--dry-run", "preview only: what would be created, the budget and the ceiling left").option("--idempotency-key <key>", "repeat a key to retry safely (default: a new UUID)").option("--confirm", "required for a real create").action(
13146
+ async (connectionId, adAccountId, options, command) => {
13147
+ const flags = getGlobalFlags(command);
13148
+ const input = parseCreateJson(options.json, CREATE_AD_SET_KEYS, [
13149
+ "campaignId",
13150
+ "name",
13151
+ "targeting"
13152
+ ]);
13153
+ const guard = guardFieldsOf(options, `create ad set "${input.name}"`);
13154
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
13155
+ const result = await client.createMetaAdSet(scopeOf(connectionId, adAccountId), {
13156
+ ...input,
13157
+ ...guard
13158
+ });
13159
+ emit(flags, result, () => {
13160
+ printCreatePreview(
13161
+ result,
13162
+ `Next: \`ads create ${connectionId} ${adAccountId} --from-scratch --json '{"adSetId":"${result.adSet?.id ?? "\u2026"}",\u2026}'\`.`
13163
+ );
13164
+ if (result.adSet) printAdSet(result.adSet);
13165
+ });
13166
+ }
13167
+ );
12150
13168
  adSet.command("update").description(
12151
13169
  "Change an ad set budget (minor units; daily XOR lifetime), schedule end or status \u2014 applies to every ad in the set. Requires --confirm."
12152
13170
  ).argument(...CONNECTION_ARG).argument(...AD_ACCOUNT_ARG).argument("<adSetId>", "ad set id (from `ads ad-sets`)").option("--daily-budget <minor>", "daily budget in minor units").option("--lifetime-budget <minor>", "lifetime budget in minor units (needs an end time)").option("--end-time <iso>", "schedule end, ISO 8601").option("--status <status>", `ad set status (${SETTABLE_STATUSES.join(" | ")})`).option("--confirm", "required \u2014 acknowledge the change on the live ad account").action(
@@ -12186,13 +13204,37 @@ More ads available \u2014 re-run with --after ${result.after}
12186
13204
  }
12187
13205
  );
12188
13206
  ads.command("create").description(
12189
- "Create an ad by duplicating an existing one (same ad set and page) with creative overrides from --json. The new ad is always created PAUSED. Requires --confirm."
13207
+ "Create an ad, always PAUSED. Default: duplicate an existing ad (same ad set and page) with creative overrides from --json. --from-scratch: a new single-image ad in an ad set (image URL, Page, copy, link). Requires --confirm (not for --dry-run)."
12190
13208
  ).argument(...CONNECTION_ARG).argument(...AD_ACCOUNT_ARG).requiredOption(
12191
13209
  "--json <fileOrJson>",
12192
- `inline JSON object or .json path: sourceAdId, name (required); message, headline, description, callToActionType, link, imageUrl (optional) ${JSON_FLAG_HINT}`
12193
- ).option("--confirm", "required \u2014 acknowledge creating an ad on the live ad account").action(
13210
+ `duplicate: sourceAdId, name (required); message, headline, description, callToActionType, link, imageUrl. --from-scratch: adSetId, name, pageId, imageUrl, message, link (required); instagramUserId, headline, description, callToActionType, urlTags ${JSON_FLAG_HINT}`
13211
+ ).option("--from-scratch", "build a new ad instead of duplicating one (fr-0312)").option("--dry-run", "--from-scratch only: preview, nothing created").option("--idempotency-key <key>", "--from-scratch only: repeat a key to retry safely").option("--confirm", "required \u2014 acknowledge creating an ad on the live ad account").action(
12194
13212
  async (connectionId, adAccountId, options, command) => {
12195
13213
  const flags = getGlobalFlags(command);
13214
+ if (options.fromScratch) {
13215
+ const scratch = parseCreateJson(
13216
+ options.json,
13217
+ CREATE_AD_FROM_SCRATCH_KEYS,
13218
+ ["adSetId", "name", "pageId", "imageUrl", "message", "link"]
13219
+ );
13220
+ const guard = guardFieldsOf(options, `create ad "${scratch.name}"`);
13221
+ const scratchClient = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
13222
+ const created = await scratchClient.createMetaAdFromScratch(
13223
+ scopeOf(connectionId, adAccountId),
13224
+ { ...scratch, ...guard }
13225
+ );
13226
+ emit(flags, created, () => {
13227
+ printCreatePreview(
13228
+ created,
13229
+ `Turn it on with \`ads update ${connectionId} ${adAccountId} ${created.ad?.id ?? "\u2026"} --status ACTIVE --confirm\` (it spends).`
13230
+ );
13231
+ if (created.ad) printAd(created.ad);
13232
+ });
13233
+ return;
13234
+ }
13235
+ if (options.dryRun || options.idempotencyKey) {
13236
+ throw new Error("--dry-run and --idempotency-key apply to --from-scratch only.");
13237
+ }
12196
13238
  const input = parseCreateAdInput(options.json);
12197
13239
  requireConfirm2(options.confirm, `create ad "${input.name}" from ${input.sourceAdId}`);
12198
13240
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -13091,6 +14133,8 @@ function promptHidden(question) {
13091
14133
 
13092
14134
  // src/commands/auth/register.ts
13093
14135
  var BOT_CHECK_FAILED_CODE = "auth/bot-check-failed";
14136
+ var DEVICE_VERIFICATION_REQUIRED_CODE = "auth/device-verification-required";
14137
+ var DEVICE_CONFIRMATION_NOTICE = "\nYour account is ready. Confirm this device in the Zanfia panel first:\nopening the panel in your browser; the CLI logs in once you are done.\n\n";
13094
14138
  var MAX_CODE_ATTEMPTS = 5;
13095
14139
  var MIN_PASSWORD_LENGTH = 8;
13096
14140
  var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -13250,12 +14294,22 @@ We sent a 6-digit code to ${email}. Check your inbox.
13250
14294
  codeChallengeMethod: "S256",
13251
14295
  hostname: safeHostname2()
13252
14296
  };
13253
- const minted = await postJson2(
13254
- "Authorizing CLI",
13255
- `${urls.usersUrl}/cli/auth/codes`,
13256
- codeBody,
13257
- { bearer: signIn.idToken, verbose }
13258
- );
14297
+ let minted;
14298
+ try {
14299
+ minted = await postJson2(
14300
+ "Authorizing CLI",
14301
+ `${urls.usersUrl}/cli/auth/codes`,
14302
+ codeBody,
14303
+ { bearer: signIn.idToken, verbose }
14304
+ );
14305
+ } catch (error) {
14306
+ if (error instanceof HttpJsonError && error.code === DEVICE_VERIFICATION_REQUIRED_CODE) {
14307
+ process.stdout.write(DEVICE_CONFIRMATION_NOTICE);
14308
+ await runLogin({ apiUrl: options.apiUrl, verbose });
14309
+ return;
14310
+ }
14311
+ throw error;
14312
+ }
13259
14313
  const exchangeBody = {
13260
14314
  code: minted.code,
13261
14315
  codeVerifier
@@ -13306,6 +14360,9 @@ plus its personal workspace are created.
13306
14360
  Running it with an email that already has a creator account simply signs
13307
14361
  that account in.
13308
14362
 
14363
+ When Zanfia asks you to confirm this device first, the CLI opens the panel in
14364
+ your browser; confirm the device there and the CLI logs in like \`login\`.
14365
+
13309
14366
  Skipping the password is fine \u2014 you can always sign in with an emailed code.
13310
14367
  `
13311
14368
  ).action(async (options) => {
@@ -14311,6 +15368,7 @@ var GRANT_PAYMENT_METHODS = [
14311
15368
  "cash",
14312
15369
  "other"
14313
15370
  ];
15371
+ var HELD_EVENTS_DECISIONS = ["send", "skip"];
14314
15372
  var AIImageAspectRatio = /* @__PURE__ */ ((AIImageAspectRatio2) => {
14315
15373
  AIImageAspectRatio2["Square"] = "1:1";
14316
15374
  AIImageAspectRatio2["Portrait"] = "4:5";
@@ -14324,6 +15382,13 @@ var AIImageQuality = /* @__PURE__ */ ((AIImageQuality2) => {
14324
15382
  AIImageQuality2["Ultra"] = "4K";
14325
15383
  return AIImageQuality2;
14326
15384
  })(AIImageQuality || {});
15385
+ var STUDIO_OUTPUT_PLATFORMS = [
15386
+ "facebook",
15387
+ "linkedin",
15388
+ "twitch",
15389
+ "kick",
15390
+ "other"
15391
+ ];
14327
15392
  var MANAGED_CONVERSION_TRACKING_CLIENTS = [
14328
15393
  "googleAnalytics",
14329
15394
  "googleTagManager",
@@ -14410,6 +15475,89 @@ var CLIENT_TAG_COLORS = [
14410
15475
  "purple"
14411
15476
  ];
14412
15477
  var TRANSCRIPTION_LANGUAGES = ["pl", "en", "default"];
15478
+ var COMMUNITY_JOIN_APPROVE_MAX_IDS = 20;
15479
+ var COMMUNITY_JOIN_DECLINE_MAX_IDS = 100;
15480
+ var LESSON_QA_INBOX_FILTERS = [
15481
+ "unanswered",
15482
+ "all",
15483
+ "public",
15484
+ "private",
15485
+ "hidden"
15486
+ ];
15487
+ var SETTABLE_LESSON_QA_ENTRY_STATUSES = [
15488
+ "published",
15489
+ "hidden"
15490
+ ];
15491
+ var CONSULTATION_BOOKING_STATUSES = [
15492
+ "booked",
15493
+ "cancelled",
15494
+ "done"
15495
+ ];
15496
+ var STUDIO_SOCIAL_DRAFT_WEAK_REASONS = [
15497
+ "too-generic",
15498
+ "not-my-voice",
15499
+ "too-long",
15500
+ "too-salesy",
15501
+ "sounds-like-ai",
15502
+ "wrong-topic"
15503
+ ];
15504
+ var YOUTUBE_PRIVACY_STATUSES = [
15505
+ "private",
15506
+ "unlisted",
15507
+ "public"
15508
+ ];
15509
+
15510
+ // src/commands/clients/automationRuns.ts
15511
+ function stepLabel(run) {
15512
+ const step = run.currentStep;
15513
+ if (!step) return "";
15514
+ return step.name ?? step.actionType ?? step.kind ?? step.nodeId;
15515
+ }
15516
+ function printStoppedRuns(result) {
15517
+ process.stdout.write(
15518
+ `Stopped ${result.stopped.length} run(s)${result.alreadyFinished > 0 ? `; ${result.alreadyFinished} had already finished` : ""}.
15519
+ `
15520
+ );
15521
+ for (const run of result.stopped) {
15522
+ process.stdout.write(` - run ${run.runId} (workflow ${run.automationId})
15523
+ `);
15524
+ }
15525
+ }
15526
+ function registerClientAutomationRunsCommands(clients2) {
15527
+ const runs = clients2.command("automation-runs").description(
15528
+ "One customer's workflow runs in progress (the customer card \u2192 \u201EAutomatyzacje w toku\u201D): list them or take the customer out"
15529
+ );
15530
+ runs.command("list").description(
15531
+ "Live runs of one customer across every workflow, newest first (test runs excluded)"
15532
+ ).argument("<clientId>", "workspace client id (wcid)").action(async (clientId, _, command) => {
15533
+ const flags = getGlobalFlags(command);
15534
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
15535
+ const result = await client.listClientAutomationRuns(clientId);
15536
+ emit(flags, result, () => {
15537
+ printTable(
15538
+ result.items.map((run) => ({
15539
+ runId: run.runId,
15540
+ workflow: run.automationName ?? `${run.automationId} (deleted)`,
15541
+ workflowId: run.automationId,
15542
+ status: run.status,
15543
+ step: stepLabel(run),
15544
+ nextStepAt: run.nextScheduledAt ?? "",
15545
+ startedAt: run.startedAt
15546
+ })),
15547
+ ["runId", "workflow", "workflowId", "status", "step", "nextStepAt", "startedAt"]
15548
+ );
15549
+ });
15550
+ });
15551
+ runs.command("stop").description(
15552
+ "Take the customer out of EVERY workflow run in progress \u2014 each run ends cancelled and no further step runs (a step already sending finishes). They can enter again when a trigger fires later. To stop one run: `workflows runs stop`. Requires --confirm"
15553
+ ).argument("<clientId>", "workspace client id (wcid)").option("--confirm", "required \u2014 acknowledge that every live run of this customer ends").action(async (clientId, options, command) => {
15554
+ const flags = getGlobalFlags(command);
15555
+ requireConfirm(options.confirm, `stop every workflow run of client ${clientId}`);
15556
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
15557
+ const result = await client.stopClientAutomationRuns(clientId);
15558
+ emit(flags, result, () => printStoppedRuns(result));
15559
+ });
15560
+ }
14413
15561
 
14414
15562
  // src/commands/clients/emails.ts
14415
15563
  import { readFileSync as readFileSync6 } from "fs";
@@ -14591,6 +15739,42 @@ function registerClientEmailsCommands(clients2) {
14591
15739
  });
14592
15740
  }
14593
15741
  );
15742
+ emails.command("delete-drafts").description(
15743
+ "Delete DRAFTS (and failed hand-offs) across customers: the listed ids, every draft with one subject, or all. Sent e-mails are never touched. --preview only counts; a delete needs --confirm and never removes more than it counted"
15744
+ ).option("--ids <a,b,...>", "draft ids (from `clients emails inbox`)", parseCommaList("--ids")).option("--subject <subject>", "every draft whose subject is exactly this").option("--all", "every draft of the workspace").option("--preview", "count the matching drafts, delete nothing").option("--confirm", "required to delete \u2014 acknowledge the drafts are deleted for good").action(
15745
+ async (options, command) => {
15746
+ const flags = getGlobalFlags(command);
15747
+ const scope = draftsScope(options);
15748
+ if (!options.preview) requireConfirm(options.confirm, "delete client e-mail drafts");
15749
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
15750
+ const counted = await client.deleteClientEmailDrafts({ ...scope, preview: true });
15751
+ if (options.preview || counted.matchedCount === 0) {
15752
+ emit(flags, counted, () => printDraftsCount(counted, "match"));
15753
+ return;
15754
+ }
15755
+ const result = await client.deleteClientEmailDrafts({
15756
+ ...scope,
15757
+ maxCount: counted.matchedCount
15758
+ });
15759
+ emit(flags, result, () => printDraftsCount(result, "deleted"));
15760
+ }
15761
+ );
15762
+ }
15763
+ function draftsScope(options) {
15764
+ const chosen = [options.ids !== void 0, options.subject !== void 0, options.all === true];
15765
+ if (chosen.filter(Boolean).length !== 1) {
15766
+ throw new Error("Pass exactly one of --ids, --subject or --all.");
15767
+ }
15768
+ if (options.ids !== void 0) return { scope: "ids", emailIds: options.ids };
15769
+ if (options.subject !== void 0) return { scope: "subject", subject: options.subject };
15770
+ return { scope: "all" };
15771
+ }
15772
+ function printDraftsCount(result, verb) {
15773
+ const count2 = verb === "deleted" ? result.deletedCount : result.matchedCount;
15774
+ process.stdout.write(
15775
+ `${count2} draft(s) ${verb === "deleted" ? "deleted" : "match (nothing deleted)"}${result.sampleSubjects.length > 0 ? `, e.g. "${result.sampleSubjects.join('", "')}"` : ""}.
15776
+ `
15777
+ );
14594
15778
  }
14595
15779
 
14596
15780
  // src/commands/clients/subscription.ts
@@ -15079,59 +16263,70 @@ function readImportCsv(csvFile) {
15079
16263
  }
15080
16264
  return contacts;
15081
16265
  }
15082
- function printMergePreview(preview) {
16266
+ function printMergePreview(preview2) {
15083
16267
  process.stdout.write("Merge preview (dry run \u2014 nothing was written)\n");
15084
- process.stdout.write(` Survivor: ${preview.survivorWcid} (${preview.survivorEmail})
15085
- `);
15086
- process.stdout.write(` Duplicate (tombstoned): ${preview.loserWcid} (${preview.loserEmail})
16268
+ process.stdout.write(` Survivor: ${preview2.survivorWcid} (${preview2.survivorEmail})
15087
16269
  `);
15088
- if (preview.blocked !== null) {
15089
- process.stdout.write(` \u26A0\uFE0F BLOCKED \u2014 live subscription(s); the merge will be refused (409):
16270
+ process.stdout.write(` Duplicate (tombstoned): ${preview2.loserWcid} (${preview2.loserEmail})
15090
16271
  `);
15091
- for (const sub of preview.blocked.subscriptions) {
16272
+ if (preview2.blocked !== null) {
16273
+ process.stdout.write(
16274
+ preview2.blocked.reason === "subscription-vs-lifetime" ? ` \u26A0\uFE0F BLOCKED \u2014 one client has a subscription, the other lifetime access to the same product; the merge will be refused (409). End the subscription first, or contact Zanfia:
16275
+ ` : ` \u26A0\uFE0F BLOCKED \u2014 live subscription(s); the merge will be refused (409):
16276
+ `
16277
+ );
16278
+ for (const sub of preview2.blocked.subscriptions) {
15092
16279
  process.stdout.write(
15093
16280
  ` - ${sub.side}: subscription ${sub.subscriptionId} on product ${sub.productId}
15094
16281
  `
15095
16282
  );
15096
16283
  }
16284
+ if (preview2.blocked.reason === "subscription-vs-lifetime") {
16285
+ for (const grant of preview2.blocked.lifetimeGrants) {
16286
+ process.stdout.write(
16287
+ ` - ${grant.side}: lifetime access to product ${grant.productId}
16288
+ `
16289
+ );
16290
+ }
16291
+ }
15097
16292
  }
15098
16293
  process.stdout.write(
15099
- ` Money: ${preview.money.survivorTotalSpentMinor} + ${preview.money.loserTotalSpentMinor} = ${preview.money.sumMinor} (minor units); ${preview.money.ordersMovingCount} order${preview.money.ordersMovingCount === 1 ? "" : "s"} moving
16294
+ ` Money: ${preview2.money.survivorTotalSpentMinor} + ${preview2.money.loserTotalSpentMinor} = ${preview2.money.sumMinor} (minor units); ${preview2.money.ordersMovingCount} order${preview2.money.ordersMovingCount === 1 ? "" : "s"} moving
15100
16295
  `
15101
16296
  );
15102
- if (preview.grants.length > 0) {
15103
- process.stdout.write(` Product access grants (${preview.grants.length}):
16297
+ if (preview2.grants.length > 0) {
16298
+ process.stdout.write(` Product access grants (${preview2.grants.length}):
15104
16299
  `);
15105
- for (const grant of preview.grants) {
16300
+ for (const grant of preview2.grants) {
15106
16301
  process.stdout.write(
15107
16302
  ` - ${grant.productName} (${grant.productId}): ${grant.action} \u2014 ${grant.reason}
15108
16303
  `
15109
16304
  );
15110
16305
  }
15111
16306
  }
15112
- if (preview.profileOverwrites.length > 0) {
16307
+ if (preview2.profileOverwrites.length > 0) {
15113
16308
  process.stdout.write(" Profile fields where the duplicate's value is DISCARDED:\n");
15114
- for (const overwrite of preview.profileOverwrites) {
16309
+ for (const overwrite of preview2.profileOverwrites) {
15115
16310
  process.stdout.write(
15116
16311
  ` - ${overwrite.field}: keeping "${overwrite.survivorValueKept ?? ""}", discarding "${overwrite.loserValueDiscarded ?? ""}"${overwrite.downstreamUnlink ? " (downstream unlink)" : ""}
15117
16312
  `
15118
16313
  );
15119
16314
  }
15120
16315
  }
15121
- if (preview.community.communitiesTouchedCount > 0) {
16316
+ if (preview2.community.communitiesTouchedCount > 0) {
15122
16317
  process.stdout.write(
15123
- ` Communities touched: ${preview.community.communitiesTouchedCount}; ~${preview.community.authoredRePointCount} authored posts/comments re-pointed
16318
+ ` Communities touched: ${preview2.community.communitiesTouchedCount}; ~${preview2.community.authoredRePointCount} authored posts/comments re-pointed
15124
16319
  `
15125
16320
  );
15126
16321
  }
15127
16322
  process.stdout.write(
15128
- ` Resulting previous emails: ${preview.resultingPreviousEmails.join(", ") || "(none)"}
16323
+ ` Resulting previous emails: ${preview2.resultingPreviousEmails.join(", ") || "(none)"}
15129
16324
  `
15130
16325
  );
15131
16326
  process.stdout.write("\n This merge is IRREVERSIBLE once executed.\n");
15132
- if (preview.blocked === null) {
16327
+ if (preview2.blocked === null) {
15133
16328
  process.stdout.write(
15134
- ` To execute: clients merge --survivor ${preview.survivorWcid} --duplicate ${preview.loserWcid} --preview-hash ${preview.previewHash}
16329
+ ` To execute: clients merge --survivor ${preview2.survivorWcid} --duplicate ${preview2.loserWcid} --preview-hash ${preview2.previewHash}
15135
16330
  `
15136
16331
  );
15137
16332
  }
@@ -15598,7 +16793,7 @@ Next page: --cursor ${page.nextCursor}
15598
16793
  emit(flags, result, () => printMergePreview(result.preview));
15599
16794
  });
15600
16795
  clients2.command("merge").description(
15601
- "\u26A0\uFE0F IRREVERSIBLY merge the duplicate client into the survivor \u2014 grants move/dedupe, community content re-points, and the duplicate is TOMBSTONED (no undo). Run `clients merge-preview` first and pass its previewHash; a stale hash is refused (409). Live subscriptions on either side block the merge"
16796
+ "\u26A0\uFE0F IRREVERSIBLY merge the duplicate client into the survivor \u2014 grants move/dedupe, community content re-points, and the duplicate is TOMBSTONED (no undo). Run `clients merge-preview` first and pass its previewHash; a stale hash is refused (409). A live subscription on both sides of one product blocks the merge, and so does a live subscription on one side against lifetime access on the other (409)"
15602
16797
  ).requiredOption("--survivor <clientId>", "the client that remains (wcid)").requiredOption("--duplicate <clientId>", "the client merged in and tombstoned (wcid)").requiredOption(
15603
16798
  "--preview-hash <hash>",
15604
16799
  "previewHash from a fresh `clients merge-preview` for the same pair"
@@ -15854,6 +17049,7 @@ Next page: --cursor ${page.nextCursor}
15854
17049
  });
15855
17050
  registerClientSubscriptionCommands(clients2);
15856
17051
  registerClientEmailsCommands(clients2);
17052
+ registerClientAutomationRunsCommands(clients2);
15857
17053
  }
15858
17054
 
15859
17055
  // src/commands/communities/index.ts
@@ -15971,6 +17167,100 @@ var MODERATION_COLUMNS = [
15971
17167
  "createdAt",
15972
17168
  "status"
15973
17169
  ];
17170
+ function joinRequestRow(request) {
17171
+ return {
17172
+ id: request.id,
17173
+ name: [request.firstName, request.lastName].filter(Boolean).join(" "),
17174
+ email: request.email,
17175
+ status: request.status,
17176
+ requestedAt: request.requestedAt,
17177
+ answers: request.answers.length,
17178
+ aiHint: request.aiHint?.verdict ?? ""
17179
+ };
17180
+ }
17181
+ var JOIN_REQUEST_COLUMNS = [
17182
+ "id",
17183
+ "name",
17184
+ "email",
17185
+ "status",
17186
+ "requestedAt",
17187
+ "answers",
17188
+ "aiHint"
17189
+ ];
17190
+ function assertIdCount(ids, max) {
17191
+ if (ids.length > max) {
17192
+ throw new Error(
17193
+ `Pass at most ${max} request ids per call (got ${ids.length}); run it again for the rest.`
17194
+ );
17195
+ }
17196
+ }
17197
+ function printJoinDecisions(result) {
17198
+ printTable(
17199
+ result.results.map((r) => ({
17200
+ requestId: r.requestId,
17201
+ outcome: r.outcome,
17202
+ message: r.message ?? ""
17203
+ })),
17204
+ ["requestId", "outcome", "message"]
17205
+ );
17206
+ }
17207
+ var DIGEST_OUTCOME_LABELS = {
17208
+ approved: "Approved \u2014 the digest goes out Thursday 09:00.",
17209
+ approvedSendingNow: "Approved \u2014 the send starts now.",
17210
+ alreadyApproved: "Already approved \u2014 nothing changed.",
17211
+ skipped: "Skipped \u2014 no digest goes out this week.",
17212
+ alreadySkipped: "Already skipped \u2014 nothing changed."
17213
+ };
17214
+ function digestRow(draft) {
17215
+ return {
17216
+ week: draft.weekKey,
17217
+ status: draft.skipReason ? `${draft.status} (${draft.skipReason})` : draft.status,
17218
+ sendAt: draft.sendAt,
17219
+ sentAt: draft.sentAt ?? "",
17220
+ recipients: draft.recipientsCount ?? ""
17221
+ };
17222
+ }
17223
+ var DIGEST_COLUMNS = ["week", "status", "sendAt", "sentAt", "recipients"];
17224
+ function printDigestDraft(draft) {
17225
+ process.stdout.write(
17226
+ `Week ${draft.weekKey}: ${draft.status}${draft.skipReason ? ` (${draft.skipReason})` : ""}
17227
+ `
17228
+ );
17229
+ process.stdout.write(` Send at: ${draft.sendAt} (${draft.timezone})
17230
+ `);
17231
+ process.stdout.write(` Approve by: ${draft.approvalDeadline}
17232
+ `);
17233
+ const actions = [draft.canApprove ? "approve" : null, draft.canSkip ? "skip" : null].filter(
17234
+ Boolean
17235
+ );
17236
+ process.stdout.write(` Possible now: ${actions.length > 0 ? actions.join(", ") : "nothing"}
17237
+ `);
17238
+ if (draft.sentAt) {
17239
+ process.stdout.write(` Sent: ${draft.sentAt} to ${draft.recipientsCount ?? 0} member(s)
17240
+ `);
17241
+ }
17242
+ const { preview: preview2 } = draft;
17243
+ if (!preview2) return;
17244
+ const { stats } = preview2;
17245
+ process.stdout.write(
17246
+ ` Preview (${preview2.communityName}): ${stats.postsCount} posts, ${stats.commentsCount} comments, ${stats.newMembersCount} new members
17247
+ `
17248
+ );
17249
+ for (const line of preview2.summaryLinesEn ?? preview2.summaryLinesPl ?? []) {
17250
+ process.stdout.write(` ${line.emoji} ${line.text}
17251
+ `);
17252
+ }
17253
+ for (const post of preview2.posts) {
17254
+ process.stdout.write(
17255
+ ` - ${post.title ?? post.excerpt} (${post.channelName}, ${post.authorName})
17256
+ `
17257
+ );
17258
+ }
17259
+ for (const event of preview2.events) {
17260
+ process.stdout.write(` event: ${event.title} \u2014 ${event.dateFormatted}
17261
+ `);
17262
+ }
17263
+ }
15974
17264
  function authorLabel(author) {
15975
17265
  const name = [author.firstName, author.lastName].filter(Boolean).join(" ");
15976
17266
  return author.nickname || name || author.id;
@@ -16716,7 +18006,7 @@ Next page: --cursor ${result.nextCursor}
16716
18006
  });
16717
18007
  });
16718
18008
  settings.command("update").description(
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."
18009
+ "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 (incl. xpRules: XP per activity, only the activities listed change, and xpTemplate: a preset for the whole table, with xpRules = the preset with those rows changed; send gamification.enabled with them), moderation, assistant, digest. Nested blocks in the JSON patch merge into the stored block field by field (lists such as joinQuestions are replaced whole); gamification.enabled and a moderation threshold can also be set with the --gamification / --auto-hide-after-reports flags."
16720
18010
  ).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
18011
  "--event-announcements <on|off>",
16722
18012
  "bell + e-mail to members when a new event is published (default on)"
@@ -16752,8 +18042,11 @@ Next page: --cursor ${result.nextCursor}
16752
18042
  const needsCurrent = options.gamification !== void 0 || options.autoHideAfterReports !== void 0;
16753
18043
  const current = needsCurrent ? (await client.getCommunitySettings(communityId)).settings ?? {} : {};
16754
18044
  if (options.gamification !== void 0) {
18045
+ const { xpRules, xpTemplate, courseXp, ...storedBlock } = current.gamification ?? {
18046
+ enabled: false
18047
+ };
16755
18048
  patch.gamification = {
16756
- ...patch.gamification ?? current.gamification ?? {},
18049
+ ...patch.gamification ?? storedBlock,
16757
18050
  enabled: parseOnOff(options.gamification, "--gamification")
16758
18051
  };
16759
18052
  }
@@ -17026,6 +18319,98 @@ Not going (${result.notGoingCount}):
17026
18319
  }
17027
18320
  });
17028
18321
  });
18322
+ const joinRequests = communities2.command("join-requests").description(
18323
+ "People waiting to join a community that approves members by hand: list, approve, decline (workspace staff with member management; member keys get 403)"
18324
+ );
18325
+ joinRequests.command("list").description("Pending requests first (with their answers and the AI hint), then declined ones").argument("<communityId>", "community id").action(async (communityId, _, command) => {
18326
+ const flags = getGlobalFlags(command);
18327
+ const result = await clientFor(flags).listCommunityJoinRequests(communityId);
18328
+ emit(flags, result, () => {
18329
+ process.stdout.write(`Pending: ${result.pendingCount}
18330
+ `);
18331
+ printTable(result.requests.map(joinRequestRow), JOIN_REQUEST_COLUMNS);
18332
+ for (const request of result.requests) {
18333
+ if (request.status !== "pending" || request.answers.length === 0) {
18334
+ continue;
18335
+ }
18336
+ process.stdout.write(`
18337
+ Answers of ${request.id}:
18338
+ `);
18339
+ for (const answer of request.answers) {
18340
+ process.stdout.write(` ${answer.question}
18341
+ ${answer.answer}
18342
+ `);
18343
+ }
18344
+ }
18345
+ });
18346
+ });
18347
+ joinRequests.command("approve").description(
18348
+ `Approve requests \u2014 runs the normal join (welcome e-mail with the login link, welcome DM, notifications). 1-${COMMUNITY_JOIN_APPROVE_MAX_IDS} ids per call; repeats change nothing. Requires --confirm`
18349
+ ).argument("<communityId>", "community id").argument("<requestIds...>", "request ids (from `communities join-requests list`)").option("--confirm", "required \u2014 acknowledge that the people join and get the welcome e-mail").action(
18350
+ async (communityId, requestIds, options, command) => {
18351
+ const flags = getGlobalFlags(command);
18352
+ assertIdCount(requestIds, COMMUNITY_JOIN_APPROVE_MAX_IDS);
18353
+ requireConfirm5(
18354
+ options.confirm,
18355
+ `approve ${requestIds.length} join request(s) (they join and get the welcome e-mail)`
18356
+ );
18357
+ const result = await clientFor(flags).approveCommunityJoinRequests(communityId, requestIds);
18358
+ emit(flags, result, () => printJoinDecisions(result));
18359
+ }
18360
+ );
18361
+ joinRequests.command("decline").description(
18362
+ `Decline requests quietly (a neutral e-mail, no reason given). 1-${COMMUNITY_JOIN_DECLINE_MAX_IDS} ids per call; repeats change nothing. Requires --confirm`
18363
+ ).argument("<communityId>", "community id").argument("<requestIds...>", "request ids (from `communities join-requests list`)").option("--confirm", "required \u2014 acknowledge that the people are declined and e-mailed").action(
18364
+ async (communityId, requestIds, options, command) => {
18365
+ const flags = getGlobalFlags(command);
18366
+ assertIdCount(requestIds, COMMUNITY_JOIN_DECLINE_MAX_IDS);
18367
+ requireConfirm5(options.confirm, `decline ${requestIds.length} join request(s)`);
18368
+ const result = await clientFor(flags).declineCommunityJoinRequests(communityId, requestIds);
18369
+ emit(flags, result, () => printJoinDecisions(result));
18370
+ }
18371
+ );
18372
+ const digest = communities2.command("digest").description(
18373
+ "The weekly digest e-mail to members: nothing goes out until the owner or an admin approves the week (Thursday 09:00 send, closes at the end of Thursday)"
18374
+ );
18375
+ digest.command("list").description("This week's draft with the preview of what members get, then the earlier weeks").argument("<communityId>", "community id").action(async (communityId, _, command) => {
18376
+ const flags = getGlobalFlags(command);
18377
+ const result = await clientFor(flags).listCommunityDigestDrafts(communityId);
18378
+ emit(flags, result, () => {
18379
+ if (!result.digestEnabled) {
18380
+ process.stdout.write(
18381
+ "The weekly digest is OFF for this community \u2014 no drafts are built. Turn it on in the community settings.\n"
18382
+ );
18383
+ }
18384
+ if (result.current) {
18385
+ printDigestDraft(result.current);
18386
+ } else {
18387
+ process.stdout.write("No draft this week.\n");
18388
+ }
18389
+ if (result.history.length > 0) {
18390
+ process.stdout.write("\nEarlier weeks:\n");
18391
+ printTable(result.history.map(digestRow), DIGEST_COLUMNS);
18392
+ }
18393
+ });
18394
+ });
18395
+ const decideDigest = (decision, what) => async (communityId, options, command) => {
18396
+ const flags = getGlobalFlags(command);
18397
+ requireConfirm5(options.confirm, `${what} of community ${communityId}`);
18398
+ const result = await clientFor(flags).decideCommunityDigestDraft(communityId, {
18399
+ decision,
18400
+ ...options.week !== void 0 ? { weekKey: options.week } : {}
18401
+ });
18402
+ emit(flags, result, () => {
18403
+ process.stdout.write(`${DIGEST_OUTCOME_LABELS[result.outcome]}
18404
+ `);
18405
+ printDigestDraft(result.draft);
18406
+ });
18407
+ };
18408
+ digest.command("approve").description(
18409
+ "Approve the week \u2014 the digest goes out Thursday 09:00 (at once when approved later that Thursday) to every member who gets it. Owner / admin only; repeats never send twice. Requires --confirm"
18410
+ ).argument("<communityId>", "community id").option("--week <weekKey>", "the week (from `communities digest list`); default this week").option("--confirm", "required \u2014 acknowledge that the digest is e-mailed to the members").action(decideDigest("approve", "approve the weekly digest (members are e-mailed)"));
18411
+ digest.command("skip").description(
18412
+ "Skip the week \u2014 no digest goes out (refused once the send has started). Owner / admin only. Requires --confirm"
18413
+ ).argument("<communityId>", "community id").option("--week <weekKey>", "the week (from `communities digest list`); default this week").option("--confirm", "required \u2014 acknowledge that no digest goes out that week").action(decideDigest("skip", "skip the weekly digest"));
17029
18414
  const moderation = communities2.command("moderation").description("Reported posts and comments \u2014 review the queue and resolve reports (staff)");
17030
18415
  moderation.command("queue").description("Show pending reports plus the most recently resolved ones").argument("<communityId>", "community id").action(async (communityId, _, command) => {
17031
18416
  const flags = getGlobalFlags(command);
@@ -17081,10 +18466,16 @@ var SETTINGS_PATCH_KEYS = [
17081
18466
  "eventsEnabled",
17082
18467
  "eventAnnouncements",
17083
18468
  "directMessagesEnabled",
18469
+ "membersCanCreateGroups",
18470
+ "channelTypeIcons",
18471
+ "channelGroupsCollapsed",
17084
18472
  "gamification",
17085
18473
  "moderation",
17086
18474
  "assistant",
17087
- "digest"
18475
+ "digest",
18476
+ "welcomeMessage",
18477
+ "joinQuestions",
18478
+ "joinApproval"
17088
18479
  ];
17089
18480
  function parseSettingsPatch(value) {
17090
18481
  const parsed = parseJsonObject(value, "--patch");
@@ -17113,6 +18504,99 @@ function parseJsonObject(value, flag) {
17113
18504
  return parsed;
17114
18505
  }
17115
18506
 
18507
+ // src/commands/consultations/index.ts
18508
+ function assertBookingStatus(value) {
18509
+ const match = CONSULTATION_BOOKING_STATUSES.find((status) => status === value);
18510
+ if (match === void 0) {
18511
+ throw new Error(
18512
+ `Invalid --status: ${value}. Use one of: ${CONSULTATION_BOOKING_STATUSES.join(", ")}`
18513
+ );
18514
+ }
18515
+ return match;
18516
+ }
18517
+ function assertDate(value, flag) {
18518
+ if (Number.isNaN(Date.parse(value))) {
18519
+ throw new Error(`Invalid ${flag}: ${value} (expected an ISO date, e.g. 2026-11-01)`);
18520
+ }
18521
+ return value;
18522
+ }
18523
+ function printNextPage(nextCursor) {
18524
+ if (nextCursor) process.stdout.write(`
18525
+ Next page: --cursor ${nextCursor}
18526
+ `);
18527
+ }
18528
+ function registerConsultationsCommands(program2) {
18529
+ const consultations = program2.command("consultations").description(
18530
+ "Consultations booked in the Zanfia calendar: booked sessions and buyers who never booked"
18531
+ );
18532
+ consultations.command("bookings").description(
18533
+ "Booked sessions of every consultation (or --product) starting in a date range, earliest first, with the client name and e-mail"
18534
+ ).option("--product <productId>", "only this consultation product").option("--from <date>", "range start (ISO or YYYY-MM-DD; default today 00:00 UTC)").option("--to <date>", "range end, exclusive (default from + 30 days; at most 366 days)").option("--status <status>", CONSULTATION_BOOKING_STATUSES.join("|")).option(
18535
+ "--limit <n>",
18536
+ "page size (default 50, max 100)",
18537
+ parseNumberOption("--limit", NumberFormat.Integer)
18538
+ ).option("--cursor <cursor>", "cursor from a previous page").action(
18539
+ async (options, command) => {
18540
+ const flags = getGlobalFlags(command);
18541
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18542
+ const page = await client.listConsultationBookings({
18543
+ productId: options.product,
18544
+ from: options.from !== void 0 ? assertDate(options.from, "--from") : void 0,
18545
+ to: options.to !== void 0 ? assertDate(options.to, "--to") : void 0,
18546
+ status: options.status !== void 0 ? assertBookingStatus(options.status) : void 0,
18547
+ limit: options.limit,
18548
+ cursor: options.cursor
18549
+ });
18550
+ emit(flags, page, () => {
18551
+ process.stdout.write(`Sessions starting ${page.from} .. ${page.to}
18552
+ `);
18553
+ printTable(
18554
+ page.items.map((b) => ({
18555
+ bookingId: b.bookingId,
18556
+ product: b.productName ?? b.productId,
18557
+ start: b.startAt,
18558
+ minutes: b.durationMinutes,
18559
+ client: b.clientName ? `${b.clientName} <${b.clientEmail}>` : b.clientEmail,
18560
+ status: b.status,
18561
+ location: b.location
18562
+ })),
18563
+ ["bookingId", "product", "start", "minutes", "client", "status", "location"]
18564
+ );
18565
+ printNextPage(page.nextCursor);
18566
+ });
18567
+ }
18568
+ );
18569
+ consultations.command("without-booking").description(
18570
+ "Buyers with active access to a consultation (every one, or --product) who never booked a session (a cancelled booking does not count). External-link consultations are skipped"
18571
+ ).option("--product <productId>", "only this consultation product").option(
18572
+ "--limit <n>",
18573
+ "page size (default 50, max 100)",
18574
+ parseNumberOption("--limit", NumberFormat.Integer)
18575
+ ).option("--cursor <cursor>", "cursor from a previous page").action(
18576
+ async (options, command) => {
18577
+ const flags = getGlobalFlags(command);
18578
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
18579
+ const page = await client.listConsultationClientsWithoutBooking({
18580
+ productId: options.product,
18581
+ limit: options.limit,
18582
+ cursor: options.cursor
18583
+ });
18584
+ emit(flags, page, () => {
18585
+ printTable(
18586
+ page.items.map((c) => ({
18587
+ clientId: c.clientId,
18588
+ client: c.clientName ? `${c.clientName} <${c.clientEmail}>` : c.clientEmail,
18589
+ product: c.productName,
18590
+ accessSince: c.accessGrantedAt ?? ""
18591
+ })),
18592
+ ["clientId", "client", "product", "accessSince"]
18593
+ );
18594
+ printNextPage(page.nextCursor);
18595
+ });
18596
+ }
18597
+ );
18598
+ }
18599
+
17116
18600
  // src/commands/conversations/index.ts
17117
18601
  import { readFileSync as readFileSync9 } from "fs";
17118
18602
  var MAX_BROADCAST_RECIPIENTS = 5e3;
@@ -17516,6 +19000,18 @@ function assertStatus(status) {
17516
19000
  throw new Error(`Invalid status "${status}". Use one of: ${LESSON_STATUSES.join(", ")}`);
17517
19001
  }
17518
19002
  }
19003
+ function assertOneOf2(value, allowed, flag) {
19004
+ const match = allowed.find((candidate) => candidate === value);
19005
+ if (match === void 0) {
19006
+ throw new Error(`Invalid ${flag}: ${value}. Use one of: ${allowed.join(", ")}`);
19007
+ }
19008
+ return match;
19009
+ }
19010
+ var QUESTION_EXCERPT_LENGTH = 60;
19011
+ function excerpt(content) {
19012
+ const line = content.replace(/\s+/g, " ").trim();
19013
+ return line.length > QUESTION_EXCERPT_LENGTH ? `${line.slice(0, QUESTION_EXCERPT_LENGTH - 1)}\u2026` : line;
19014
+ }
17519
19015
  function collectRepeatable3(value, previous) {
17520
19016
  return [...previous, value];
17521
19017
  }
@@ -18301,6 +19797,110 @@ ${NOT_SAVED_NOTE} --moments=${options.out ?? "<file>"} --confirm\`.
18301
19797
  });
18302
19798
  }
18303
19799
  );
19800
+ const questions = courses.command("questions").description("Learners' questions under the lessons: read, reply as the team, hide, accept");
19801
+ questions.command("list").description("Questions of a course, newest first, each with its replies (private ones too)").argument("<productId>", "course product id").option("--filter <filter>", LESSON_QA_INBOX_FILTERS.join("|") + " (default all)").option("--lesson <lessonId>", "only the questions under this lesson").option("--cursor <cursor>", "cursor from a previous page").action(
19802
+ async (productId, options, command) => {
19803
+ const flags = getGlobalFlags(command);
19804
+ const filter = options.filter !== void 0 ? assertOneOf2(options.filter, LESSON_QA_INBOX_FILTERS, "--filter") : void 0;
19805
+ const result = await clientFor2(flags).listCourseQuestions(productId, {
19806
+ filter,
19807
+ lessonId: options.lesson,
19808
+ cursor: options.cursor
19809
+ });
19810
+ emit(flags, result, () => {
19811
+ printTable(
19812
+ result.questions.map((q) => ({
19813
+ id: q.id,
19814
+ lesson: q.lessonId,
19815
+ by: q.author.displayName,
19816
+ visibility: q.visibility,
19817
+ status: q.status,
19818
+ replies: q.repliesCount,
19819
+ answered: q.staffReplied ? "yes" : "no",
19820
+ accepted: q.acceptedReplyId ?? "",
19821
+ question: excerpt(q.content)
19822
+ })),
19823
+ [
19824
+ "id",
19825
+ "lesson",
19826
+ "by",
19827
+ "visibility",
19828
+ "status",
19829
+ "replies",
19830
+ "answered",
19831
+ "accepted",
19832
+ "question"
19833
+ ]
19834
+ );
19835
+ if (result.nextCursor) {
19836
+ process.stdout.write(`
19837
+ Next page: --cursor ${result.nextCursor}
19838
+ `);
19839
+ }
19840
+ });
19841
+ }
19842
+ );
19843
+ questions.command("reply").description(
19844
+ "Reply to a question as the course team \u2014 the learner sees it under the lesson and is notified. Requires --confirm"
19845
+ ).argument("<productId>", "course product id").argument("<questionId>", "question id (from `courses questions list`)").option("--content <text>", "the reply text").option("--content-file <path>", "read the reply text from a file").option("--confirm", "required \u2014 acknowledge that the learner sees the reply").action(
19846
+ async (productId, questionId, options, command) => {
19847
+ const flags = getGlobalFlags(command);
19848
+ if (options.content !== void 0 && options.contentFile !== void 0) {
19849
+ throw new Error("Use either --content or --content-file, not both.");
19850
+ }
19851
+ const content = options.contentFile !== void 0 ? readFileSync10(options.contentFile, "utf8") : options.content;
19852
+ if (content === void 0 || !content.trim()) {
19853
+ throw new Error("Pass the reply with --content or --content-file.");
19854
+ }
19855
+ requireConfirm(options.confirm, `reply to question ${questionId} (the learner sees it)`);
19856
+ const result = await clientFor2(flags).replyToCourseQuestion(productId, questionId, content);
19857
+ emit(flags, result, () => {
19858
+ process.stdout.write(`Replied to question ${questionId} (reply ${result.reply.id}).
19859
+ `);
19860
+ });
19861
+ }
19862
+ );
19863
+ questions.command("status").description(
19864
+ "Hide a question or a reply from learners (the team still sees it), or publish it again. Requires --confirm"
19865
+ ).argument("<productId>", "course product id").argument("<entryId>", "question or reply id").requiredOption("--status <status>", SETTABLE_LESSON_QA_ENTRY_STATUSES.join("|")).option("--confirm", "required \u2014 acknowledge the change learners see").action(
19866
+ async (productId, entryId, options, command) => {
19867
+ const flags = getGlobalFlags(command);
19868
+ const status = assertOneOf2(
19869
+ options.status,
19870
+ SETTABLE_LESSON_QA_ENTRY_STATUSES,
19871
+ "--status"
19872
+ );
19873
+ requireConfirm(options.confirm, `set ${entryId} to ${status}`);
19874
+ const result = await clientFor2(flags).setCourseQuestionStatus(productId, entryId, status);
19875
+ emit(flags, result, () => {
19876
+ process.stdout.write(`${result.entry.id} is now ${result.entry.status}.
19877
+ `);
19878
+ });
19879
+ }
19880
+ );
19881
+ questions.command("accept").description(
19882
+ "Mark a reply as the answer to its question (--clear removes the mark). Requires --confirm"
19883
+ ).argument("<productId>", "course product id").argument("<questionId>", "question id").option("--reply <replyId>", "the reply to mark as the answer").option("--clear", "remove the answer mark").option("--confirm", "required \u2014 acknowledge the change learners see").action(
19884
+ async (productId, questionId, options, command) => {
19885
+ const flags = getGlobalFlags(command);
19886
+ if (options.reply === void 0 === (options.clear !== true)) {
19887
+ throw new Error("Pass exactly one of --reply <replyId> or --clear.");
19888
+ }
19889
+ const replyId = options.reply ?? null;
19890
+ requireConfirm(
19891
+ options.confirm,
19892
+ replyId ? `mark reply ${replyId} as the answer` : `clear the answer of question ${questionId}`
19893
+ );
19894
+ const result = await clientFor2(flags).acceptCourseAnswer(productId, questionId, replyId);
19895
+ emit(flags, result, () => {
19896
+ process.stdout.write(
19897
+ result.acceptedReplyId ? `Reply ${result.acceptedReplyId} is the answer to question ${result.questionId}.
19898
+ ` : `Question ${result.questionId} has no accepted answer now.
19899
+ `
19900
+ );
19901
+ });
19902
+ }
19903
+ );
18304
19904
  }
18305
19905
 
18306
19906
  // src/commands/discounts/index.ts
@@ -18728,6 +20328,40 @@ function registerEmailSenderCommands(emails) {
18728
20328
  );
18729
20329
  });
18730
20330
  });
20331
+ sender.command("warmups").description(
20332
+ "Each sending domain's warm-up (KI#1651): new domains send campaigns in hourly batches for a few days; shows the state, why a domain counts as warmed and the cap per hour"
20333
+ ).action(async (_, command) => {
20334
+ const flags = getGlobalFlags(command);
20335
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
20336
+ const result = await client.getSenderDomainWarmups();
20337
+ emit(flags, result, () => printWarmups(result.warmups));
20338
+ });
20339
+ sender.command("declare-warmed").description(
20340
+ "\u201EWysy\u0142am z tej domeny w innym narz\u0119dziu\u201D: switch the warm-up of a verified domain off at once, recorded under your name. Requires --confirm and the campaigns.manage permission"
20341
+ ).argument("<domain>", "the sending domain (from `emails sender warmups`)").option("--confirm", "required \u2014 acknowledge campaigns go out from it without batches").action(async (domain, options, command) => {
20342
+ const flags = getGlobalFlags(command);
20343
+ requireConfirm(options.confirm, `switch off the warm-up of ${domain}`);
20344
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
20345
+ const result = await client.declareSenderDomainWarmed(domain);
20346
+ emit(flags, result, () => {
20347
+ process.stdout.write(`Warm-up of ${result.warmup.domain} is off.
20348
+ `);
20349
+ printWarmups([result.warmup]);
20350
+ });
20351
+ });
20352
+ }
20353
+ function printWarmups(warmups) {
20354
+ printTable(
20355
+ warmups.map((warmup) => ({
20356
+ domain: warmup.domain,
20357
+ state: warmup.state,
20358
+ reason: warmup.warmedReason ?? "",
20359
+ day: warmup.warmupDay ?? "",
20360
+ capPerHour: warmup.hourlyCap ?? "",
20361
+ canDeclare: warmup.canDeclare ? "yes" : ""
20362
+ })),
20363
+ ["domain", "state", "reason", "day", "capPerHour", "canDeclare"]
20364
+ );
18731
20365
  }
18732
20366
 
18733
20367
  // src/commands/emails/index.ts
@@ -19669,16 +21303,33 @@ function registerFormsCommands(program2) {
19669
21303
  emit(flags, result, () => {
19670
21304
  printTable(
19671
21305
  result.submissions.map((s) => ({
21306
+ id: s.id,
19672
21307
  email: s.email,
19673
21308
  firstName: s.firstName ?? "",
19674
21309
  clientId: s.clientId ?? "",
19675
21310
  language: s.language ?? "",
19676
21311
  createdAt: s.createdAt
19677
21312
  })),
19678
- ["email", "firstName", "clientId", "language", "createdAt"]
21313
+ ["id", "email", "firstName", "clientId", "language", "createdAt"]
19679
21314
  );
19680
21315
  });
19681
21316
  });
21317
+ forms.command("room-link").description(
21318
+ "E-mail a webinar registrant their personal room link again (to THEIR address; one per sign-up per 10 minutes). Requires --confirm"
21319
+ ).argument("<formId>", "the webinar sign-up form id").argument("<submissionId>", "the submission id (from `forms submissions`)").option("--confirm", "required \u2014 acknowledge the registrant is e-mailed").action(
21320
+ async (formId, submissionId, options, command) => {
21321
+ const flags = getGlobalFlags(command);
21322
+ requireConfirm(options.confirm, `e-mail the room link of submission ${submissionId}`);
21323
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21324
+ const result = await client.resendFormSubmissionRoomLink(formId, submissionId);
21325
+ emit(
21326
+ flags,
21327
+ result,
21328
+ () => process.stdout.write(`Room link e-mailed again for submission ${submissionId}.
21329
+ `)
21330
+ );
21331
+ }
21332
+ );
19682
21333
  }
19683
21334
 
19684
21335
  // src/commands/funnels/index.ts
@@ -19701,6 +21352,7 @@ var FUNNEL_TEMPLATE_IDS = [
19701
21352
  "challenge"
19702
21353
  ];
19703
21354
  var EVENT_MODES = ["live", "on_demand"];
21355
+ var OFFER_PAGE_LAYOUTS = ["digital", "physical"];
19704
21356
  var SCHEDULER_UNAVAILABLE_STATUS2 = 503;
19705
21357
  var FUNNEL_NOT_READY_STATUS = 409;
19706
21358
  function requireConfirm6(confirm, what) {
@@ -20020,6 +21672,37 @@ function registerFunnelsCommands(program2) {
20020
21672
  emit(flags, result, () => {
20021
21673
  process.stdout.write(
20022
21674
  `Generation queued for step ${stepId} (run ${result.runId}). It takes 1-5 minutes; run "funnels get ${id}" to see the page once it lands.
21675
+ `
21676
+ );
21677
+ });
21678
+ }
21679
+ );
21680
+ funnels.command("add-offer-page").description(
21681
+ 'Add a designed one-time-offer page right AFTER a signup page step (the dashboard canvas "Add step > Offer"): a template in the signup page colors with a 30-minute timer, "yes" pays for --checkout on the page, "no thanks" goes to the step that followed the signup. The signup redirects to it and the funnel gets the step (in place of the free checkout\'s empty OTO step right after the signup, when there is one). Created as a DRAFT page (edit it, then `pages publish`); template copy, no AI credits. A live funnel is refused: switch it to draft first.'
21682
+ ).argument("<id>", "funnel id").requiredOption(
21683
+ "--checkout <checkoutId>",
21684
+ "the PAID checkout the offer sells, with a slug (active or inactive; never a free one)"
21685
+ ).requiredOption(
21686
+ "--layout <layout>",
21687
+ `${OFFER_PAGE_LAYOUTS.join(" | ")} (digital = ebook / mini course, physical = free + shipping)`
21688
+ ).option(
21689
+ "--after <n|id>",
21690
+ "the signup step to follow: its 1-based position (`funnels get`) or stepId; default the first"
21691
+ ).option("--lang <en|pl>", "language of the template copy (default en)").action(
21692
+ async (id, options, command) => {
21693
+ const flags = getGlobalFlags(command);
21694
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21695
+ const afterStepId = options.after !== void 0 ? await resolveStepId(client, id, options.after) : void 0;
21696
+ const input = {
21697
+ checkoutId: options.checkout,
21698
+ layout: parseChoice3(options.layout, OFFER_PAGE_LAYOUTS, "--layout"),
21699
+ ...afterStepId ? { afterStepId } : {},
21700
+ ...options.lang !== void 0 ? { lang: parseChoice3(options.lang, FUNNEL_LANGS, "--lang") } : {}
21701
+ };
21702
+ const result = await client.addFunnelOfferPage(id, input);
21703
+ emit(flags, result, () => {
21704
+ process.stdout.write(
21705
+ `Added the DRAFT offer page /${result.slug} (page ${result.pageId}, step ${result.stepId}) after step ${result.afterStepId}. It names the checkout's product at its price (crossed-out only when the price has a compare-at amount), no sample claims: add what the buyer gets, then publish it.
20023
21706
  `
20024
21707
  );
20025
21708
  });
@@ -20046,7 +21729,7 @@ function registerFunnelsCommands(program2) {
20046
21729
  });
20047
21730
  });
20048
21731
  funnels.command("from-template").description(
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."
21732
+ "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 or an external one (`live-rooms create`; then --product is optional; needs the live_rooms plan feature). Requires --confirm."
20050
21733
  ).argument("<templateId>", `template id (${FUNNEL_TEMPLATE_IDS.join(" | ")})`).option(
20051
21734
  "--product <productId>",
20052
21735
  "the product the funnel sells or delivers (required, except with --live-room)"
@@ -20058,7 +21741,7 @@ function registerFunnelsCommands(program2) {
20058
21741
  "webinar (live): the meeting link (ClickMeeting, Zoom, Teams, Meet) the pages and reminders carry"
20059
21742
  ).option(
20060
21743
  "--live-room <roomId>",
20061
- "webinar: a webinar (`live-rooms list`) that plays a recording; its sessions are the event"
21744
+ "webinar: a webinar (`live-rooms list`) that plays a recording, or an external one (`live-rooms create`); its sessions are the event"
20062
21745
  ).option(
20063
21746
  "--page-template <id>",
20064
21747
  "design template for every generated page (a page-template id, e.g. course-fresh); thank-you steps take the companion"
@@ -21293,7 +22976,68 @@ to refresh it.
21293
22976
  });
21294
22977
  }
21295
22978
 
22979
+ // src/commands/liveRooms/streamKeyInput.ts
22980
+ import { readFileSync as readFileSync17 } from "fs";
22981
+ var streamKeyIo = {
22982
+ readStdin: () => readFileSync17(0, "utf8"),
22983
+ stdinIsTty: () => process.stdin.isTTY === true,
22984
+ promptHidden: (question) => promptHidden(question)
22985
+ };
22986
+ async function readStreamKey(options) {
22987
+ const { readStdin, stdinIsTty, promptHidden: ask } = streamKeyIo;
22988
+ let key;
22989
+ if (stdinIsTty()) {
22990
+ key = (await ask("Stream key (hidden): ")).trim();
22991
+ } else if (options.keyStdin) {
22992
+ key = readStdin().trim();
22993
+ } else {
22994
+ throw new Error(
22995
+ "No terminal to ask for the stream key: pipe it on stdin with --key-stdin (the key is never a command-line argument)."
22996
+ );
22997
+ }
22998
+ if (!key) throw new Error("No stream key was given.");
22999
+ return key;
23000
+ }
23001
+ var MASK = "\u2022\u2022\u2022\u2022";
23002
+ function maskedStreamKey(value) {
23003
+ return value.startsWith(MASK) && value.length <= MASK.length + 4 ? value : MASK;
23004
+ }
23005
+
21296
23006
  // src/commands/liveRooms/index.ts
23007
+ var STUDIO_SCENE_LAYOUTS = ["solo", "presentation", "grid"];
23008
+ function parseSceneLayout(value) {
23009
+ const layout = STUDIO_SCENE_LAYOUTS.find((candidate) => candidate === value);
23010
+ if (!layout) throw new Error(`--layout must be one of ${STUDIO_SCENE_LAYOUTS.join(", ")}`);
23011
+ return layout;
23012
+ }
23013
+ function parseOutputPlatform(value) {
23014
+ const platform2 = STUDIO_OUTPUT_PLATFORMS.find((candidate) => candidate === value.toLowerCase());
23015
+ if (!platform2) {
23016
+ throw new Error(`--platform must be one of ${STUDIO_OUTPUT_PLATFORMS.join(", ")}`);
23017
+ }
23018
+ return platform2;
23019
+ }
23020
+ var safeOutput = (output) => ({
23021
+ ...output,
23022
+ streamKey: maskedStreamKey(output.streamKey)
23023
+ });
23024
+ var outputRow = (output) => ({
23025
+ id: output.id,
23026
+ platform: output.platform,
23027
+ label: output.label ?? "",
23028
+ url: output.url,
23029
+ key: maskedStreamKey(output.streamKey),
23030
+ enabled: output.enabled ? "on" : "off",
23031
+ state: output.state,
23032
+ message: output.message ?? ""
23033
+ });
23034
+ var OUTPUT_COLUMNS = ["id", "platform", "label", "url", "key", "enabled", "state", "message"];
23035
+ var SENT_NOTE = '\n"sent" = the stream leaves for the platform, which does not say whether it took the key: check the live preview there.\n';
23036
+ function parseOnOff4(value) {
23037
+ if (value === "on") return true;
23038
+ if (value === "off") return false;
23039
+ throw new Error(`expected on or off, got "${value}"`);
23040
+ }
21297
23041
  var minutes = (seconds) => `${Math.round(seconds / 60)} min`;
21298
23042
  var percent = (part, whole) => whole > 0 ? `${Math.round(part / whole * 100)}%` : "-";
21299
23043
  var countOrDash = (value) => value === null || value === void 0 ? "-" : String(value);
@@ -21319,29 +23063,256 @@ function describeSchedule(room) {
21319
23063
  }
21320
23064
  return `every ${room.schedule.everyMinutes} min${until}${exclusions}`;
21321
23065
  }
23066
+ function isMeetingLink(value) {
23067
+ try {
23068
+ const url = new URL(value);
23069
+ return url.protocol === "https:" && url.hostname !== "";
23070
+ } catch {
23071
+ return false;
23072
+ }
23073
+ }
23074
+ function printRoom(result) {
23075
+ const { room } = result;
23076
+ printTable(
23077
+ [
23078
+ {
23079
+ id: room.id,
23080
+ title: room.title,
23081
+ startAt: room.startAt,
23082
+ endAt: room.endAt,
23083
+ link: room.externalUrl ?? "",
23084
+ replay: room.replay ? room.replay.name ?? room.replay.mediaId : ""
23085
+ }
23086
+ ],
23087
+ ["id", "title", "startAt", "endAt", "link", "replay"]
23088
+ );
23089
+ }
21322
23090
  function registerLiveRoomsCommands(program2) {
21323
23091
  const liveRooms = program2.command("live-rooms").description(
21324
- "Webinars (incl. rolling autowebinar sessions) \u2014 list and attendance; webinars are managed in the dashboard"
23092
+ "Webinars (incl. rolling autowebinar sessions) \u2014 list, attendance, and an external webinar (ClickMeeting, Zoom, Teams, Meet) with its recording; the rest in the dashboard"
23093
+ );
23094
+ liveRooms.command("list").description("List webinars (latest start first)").action(async (_, command) => {
23095
+ const flags = getGlobalFlags(command);
23096
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23097
+ const result = await client.listLiveRooms();
23098
+ emit(flags, result, () => {
23099
+ printTable(
23100
+ result.rooms.map((room) => ({
23101
+ id: room.id,
23102
+ title: room.title,
23103
+ source: room.source,
23104
+ status: room.endedAt ? "ended" : room.status,
23105
+ startAt: room.startAt,
23106
+ schedule: describeSchedule(room),
23107
+ access: room.access.everyone ? "everyone" : "restricted"
23108
+ })),
23109
+ ["id", "title", "source", "status", "startAt", "schedule", "access"]
23110
+ );
23111
+ });
23112
+ });
23113
+ liveRooms.command("create").description(
23114
+ "An external webinar (ClickMeeting, Zoom, Teams, Meet): the webinar page counts down, from 15 min before the start \u201EWejd\u017A na webinar\u201D opens the link and counts the person as present. Then: funnels from-template webinar --live-room <id>"
23115
+ ).requiredOption("--title <title>", "webinar title").requiredOption("--start <iso>", "start, ISO 8601 (e.g. 2026-11-27T19:00:00+01:00)").requiredOption("--join-url <url>", "the meeting link (https)").option("--duration <minutes>", "length in minutes (5-720)", "90").option("--timezone <zone>", "IANA zone the webinar is shown in", "Europe/Warsaw").option("--description <text>", "one or two sentences about it").action(
23116
+ async (options, command) => {
23117
+ const flags = getGlobalFlags(command);
23118
+ const durationMinutes = Number(options.duration);
23119
+ if (!Number.isInteger(durationMinutes) || durationMinutes < 5 || durationMinutes > 720) {
23120
+ throw new Error("--duration must be a whole number of minutes from 5 to 720");
23121
+ }
23122
+ if (Number.isNaN(Date.parse(options.start))) {
23123
+ throw new Error("--start must be an ISO 8601 date-time");
23124
+ }
23125
+ if (!isMeetingLink(options.joinUrl)) {
23126
+ throw new Error("--join-url must be an https link");
23127
+ }
23128
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23129
+ const result = await client.createExternalWebinar({
23130
+ title: options.title,
23131
+ ...options.description !== void 0 ? { description: options.description } : {},
23132
+ startAt: options.start,
23133
+ durationMinutes,
23134
+ timezone: options.timezone,
23135
+ url: options.joinUrl
23136
+ });
23137
+ emit(flags, result, () => {
23138
+ printRoom(result);
23139
+ process.stdout.write(
23140
+ `
23141
+ Next: zanfia funnels from-template webinar --live-room ${result.room.id} --lang pl --confirm (the sign-up page, thank-you page and reminders on it).
23142
+ `
23143
+ );
23144
+ });
23145
+ }
23146
+ );
23147
+ liveRooms.command("replay").description(
23148
+ "An external webinar's recording, attached after it ran (a ready Media Library video, e.g. from media upload): the webinar page plays it as the replay"
23149
+ ).argument("<roomId>", "webinar id (from live-rooms list)").option("--media <mediaId>", "the recording (Media Library video id)").option("--remove", "take the recording off").action(
23150
+ async (roomId, options, command) => {
23151
+ const flags = getGlobalFlags(command);
23152
+ if (Boolean(options.media) === Boolean(options.remove)) {
23153
+ throw new Error("pass exactly one of --media <mediaId> or --remove");
23154
+ }
23155
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23156
+ const result = await client.setLiveRoomReplay(roomId, options.media ?? null);
23157
+ emit(flags, result, () => printRoom(result));
23158
+ }
21325
23159
  );
21326
- liveRooms.command("list").description("List webinars (latest start first)").action(async (_, command) => {
23160
+ liveRooms.command("scene").description(
23161
+ "A studio webinar's broadcast picture (studio scenes, early access): layout, banner, the name under the camera, the offer and the featured chat question on screen. Only the options you pass change"
23162
+ ).argument("<roomId>", "studio webinar id (from live-rooms list)").option("--layout <layout>", "solo | presentation | grid").option("--banner <text>", "the banner along the top").option("--no-banner", "remove the banner").option("--name <name>", "the name under the camera (lower third)").option("--title <title>", "the line under the name").option("--no-lower-third", "remove the name under the camera").option("--offer <on|off>", "the webinar's offer as a card in the picture").option("--comment <on|off>", "the featured chat question in the picture").option("--logo <on|off>", "the logo in the corner").action(
23163
+ async (roomId, options, command) => {
23164
+ const flags = getGlobalFlags(command);
23165
+ const input = {
23166
+ ...options.layout !== void 0 ? { layout: parseSceneLayout(options.layout) } : {},
23167
+ ...options.banner === false ? { banner: null } : options.banner !== void 0 ? { banner: options.banner } : {},
23168
+ ...options.lowerThird === false ? { lowerThird: null } : options.name !== void 0 ? { lowerThird: { name: options.name, title: options.title ?? null } } : {},
23169
+ ...options.offer !== void 0 ? { offerOnScreen: parseOnOff4(options.offer) } : {},
23170
+ ...options.comment !== void 0 ? { commentOnScreen: parseOnOff4(options.comment) } : {},
23171
+ ...options.logo !== void 0 ? { showLogo: parseOnOff4(options.logo) } : {}
23172
+ };
23173
+ if (Object.keys(input).length === 0) {
23174
+ throw new Error("pass at least one option (e.g. --layout presentation)");
23175
+ }
23176
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23177
+ const result = await client.setLiveRoomStudioScene(roomId, input);
23178
+ emit(flags, result, () => {
23179
+ printTable(
23180
+ [
23181
+ {
23182
+ layout: result.scene.layout,
23183
+ banner: result.scene.banner ?? "",
23184
+ lowerThird: result.scene.lowerThird?.name ?? "",
23185
+ offer: result.scene.offerOnScreen ? "on" : "off",
23186
+ comment: result.scene.commentOnScreen ? "on" : "off",
23187
+ onAir: result.appliedToRoom ? "yes" : "next go-live"
23188
+ }
23189
+ ],
23190
+ ["layout", "banner", "lowerThird", "offer", "comment", "onAir"]
23191
+ );
23192
+ });
23193
+ }
23194
+ );
23195
+ const guestRow = (guest) => ({
23196
+ id: guest.id,
23197
+ name: guest.name,
23198
+ state: guest.state,
23199
+ inStudio: guest.connected ? "yes" : "no",
23200
+ muted: guest.mutedForStream ? "yes" : "no",
23201
+ expiresAt: guest.expiresAt
23202
+ });
23203
+ const GUEST_COLUMNS = ["id", "name", "state", "inStudio", "muted", "expiresAt"];
23204
+ liveRooms.command("guest-invite").description(
23205
+ "Invite a guest into a studio webinar's studio (early access): prints their personal link ONCE (only its hash is kept; nothing is sent). They join backstage, the host puts them on stage"
23206
+ ).argument("<roomId>", "studio webinar id (from live-rooms list)").requiredOption("--name <name>", "the guest's name (shown under their camera)").option("--email <email>", "for your own reference; nothing is sent to it").action(async (roomId, options, command) => {
21327
23207
  const flags = getGlobalFlags(command);
21328
23208
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
21329
- const result = await client.listLiveRooms();
23209
+ const result = await client.createLiveRoomStudioGuestInvite(roomId, {
23210
+ name: options.name,
23211
+ email: options.email ?? null
23212
+ });
21330
23213
  emit(flags, result, () => {
21331
23214
  printTable(
21332
- result.rooms.map((room) => ({
21333
- id: room.id,
21334
- title: room.title,
21335
- source: room.source,
21336
- status: room.endedAt ? "ended" : room.status,
21337
- startAt: room.startAt,
21338
- schedule: describeSchedule(room),
21339
- access: room.access.everyone ? "everyone" : "restricted"
21340
- })),
21341
- ["id", "title", "source", "status", "startAt", "schedule", "access"]
23215
+ [{ ...guestRow(result.guest), inviteUrl: result.inviteUrl }],
23216
+ [...GUEST_COLUMNS, "inviteUrl"]
23217
+ );
23218
+ });
23219
+ });
23220
+ liveRooms.command("guests").description("A studio webinar's invited guests: state, in the studio now, link expiry").argument("<roomId>", "studio webinar id (from live-rooms list)").action(async (roomId, _options, command) => {
23221
+ const flags = getGlobalFlags(command);
23222
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23223
+ const result = await client.listLiveRoomStudioGuests(roomId);
23224
+ emit(flags, result, () => {
23225
+ printTable(result.guests.map(guestRow), GUEST_COLUMNS);
23226
+ });
23227
+ });
23228
+ liveRooms.command("guest-remove").description("Remove a guest from the studio: the link stops working, they are disconnected").argument("<roomId>", "studio webinar id (from live-rooms list)").argument("<guestId>", "guest id (from live-rooms guests)").action(
23229
+ async (roomId, guestId, _options, command) => {
23230
+ const flags = getGlobalFlags(command);
23231
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23232
+ const result = await client.revokeLiveRoomStudioGuestInvite(roomId, guestId);
23233
+ emit(flags, result, () => {
23234
+ printTable([guestRow(result.guest)], GUEST_COLUMNS);
23235
+ });
23236
+ }
23237
+ );
23238
+ liveRooms.command("outputs").description(
23239
+ "A studio webinar's extra streams (Facebook, LinkedIn, Twitch, Kick, other RTMP): state and errors; the stream key shows masked"
23240
+ ).argument("<roomId>", "studio webinar id (from live-rooms list)").action(async (roomId, _options, command) => {
23241
+ const flags = getGlobalFlags(command);
23242
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23243
+ const result = await client.listLiveRoomStudioOutputs(roomId);
23244
+ const safe = { ...result, outputs: result.outputs.map(safeOutput) };
23245
+ emit(flags, safe, () => {
23246
+ printTable(safe.outputs.map(outputRow), OUTPUT_COLUMNS);
23247
+ process.stdout.write(
23248
+ `
23249
+ ${safe.outputs.length} of ${safe.maxOutputs} outputs.` + (safe.simulcastAvailable ? "" : " This webinar streams to YouTube, which does not run through Cloudflare here: extra streams need the Zanfia player as the destination.") + "\n"
21342
23250
  );
23251
+ if (safe.outputs.some((output) => output.state === "sent")) {
23252
+ process.stdout.write(SENT_NOTE);
23253
+ }
21343
23254
  });
21344
23255
  });
23256
+ liveRooms.command("output-add").description(
23257
+ "Add an extra stream to a studio webinar (early access). The stream key is asked for hidden, or read from stdin with --key-stdin; it is never an argument and never shown again. Refused (409) on a YouTube webinar that cannot run through Cloudflare"
23258
+ ).argument("<roomId>", "studio webinar id (from live-rooms list)").requiredOption("--platform <platform>", STUDIO_OUTPUT_PLATFORMS.join(" | ")).option(
23259
+ "--url <url>",
23260
+ "rtmp:// or rtmps:// ingest address (prefilled for facebook, twitch, kick; required otherwise)"
23261
+ ).option("--label <label>", 'your own name for it (e.g. "Company page")').option("--off", "add it switched off").option("--key-stdin", "read the stream key from stdin (e.g. from a password manager)").action(
23262
+ async (roomId, options, command) => {
23263
+ const flags = getGlobalFlags(command);
23264
+ const platform2 = parseOutputPlatform(options.platform);
23265
+ const streamKey = await readStreamKey({ keyStdin: options.keyStdin });
23266
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23267
+ const result = await client.createLiveRoomStudioOutput(roomId, {
23268
+ platform: platform2,
23269
+ ...options.url !== void 0 ? { url: options.url } : {},
23270
+ ...options.label !== void 0 ? { label: options.label } : {},
23271
+ ...options.off ? { enabled: false } : {},
23272
+ streamKey
23273
+ });
23274
+ const output = safeOutput(result.output);
23275
+ emit(flags, { output }, () => printTable([outputRow(output)], OUTPUT_COLUMNS));
23276
+ }
23277
+ );
23278
+ liveRooms.command("output-update").description(
23279
+ "Change an extra stream: address, name, on / off, or a NEW stream key (replace only: the stored key is never read back). Only the options you pass change"
23280
+ ).argument("<roomId>", "studio webinar id (from live-rooms list)").argument("<outputId>", "output id (from live-rooms outputs)").option("--url <url>", "new rtmp:// or rtmps:// ingest address").option("--label <label>", "your own name for it").option("--no-label", "remove your name (the platform's name shows)").option("--enabled <on|off>", "switch it on or off (works while on air)").option("--new-key", "replace the stream key: asked for hidden").option("--key-stdin", "replace the stream key with the one read from stdin").action(
23281
+ async (roomId, outputId, options, command) => {
23282
+ const flags = getGlobalFlags(command);
23283
+ const input = {
23284
+ ...options.url !== void 0 ? { url: options.url } : {},
23285
+ ...options.label === false ? { label: null } : options.label !== void 0 ? { label: options.label } : {},
23286
+ ...options.enabled !== void 0 ? { enabled: parseOnOff4(options.enabled) } : {}
23287
+ };
23288
+ if (options.newKey || options.keyStdin) {
23289
+ input.streamKey = await readStreamKey({ keyStdin: options.keyStdin });
23290
+ }
23291
+ if (Object.keys(input).length === 0) {
23292
+ throw new Error("pass at least one option (e.g. --enabled off or --new-key)");
23293
+ }
23294
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23295
+ const result = await client.updateLiveRoomStudioOutput(roomId, outputId, input);
23296
+ const output = safeOutput(result.output);
23297
+ emit(flags, { output }, () => printTable([outputRow(output)], OUTPUT_COLUMNS));
23298
+ }
23299
+ );
23300
+ liveRooms.command("output-remove").description(
23301
+ "Remove an extra stream and delete its stored stream key (stops it at once when on air). Requires --confirm"
23302
+ ).argument("<roomId>", "studio webinar id (from live-rooms list)").argument("<outputId>", "output id (from live-rooms outputs)").option("--confirm", "confirm the removal").action(
23303
+ async (roomId, outputId, options, command) => {
23304
+ if (!options.confirm) {
23305
+ throw new Error("Refusing to remove the output without --confirm.");
23306
+ }
23307
+ const flags = getGlobalFlags(command);
23308
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23309
+ const result = await client.deleteLiveRoomStudioOutput(roomId, outputId);
23310
+ emit(flags, result, () => {
23311
+ process.stdout.write(`Removed output ${outputId} and its stream key.
23312
+ `);
23313
+ });
23314
+ }
23315
+ );
21345
23316
  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) => {
21346
23317
  const flags = getGlobalFlags(command);
21347
23318
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -21404,7 +23375,7 @@ Viewers of ${options.session}:
21404
23375
  }
21405
23376
 
21406
23377
  // src/commands/media/index.ts
21407
- import { existsSync, readFileSync as readFileSync17, writeFileSync as writeFileSync4 } from "fs";
23378
+ import { existsSync, readFileSync as readFileSync18, writeFileSync as writeFileSync4 } from "fs";
21408
23379
  import { readFile as readFile3, stat as stat5 } from "fs/promises";
21409
23380
  import { basename } from "path";
21410
23381
 
@@ -24576,7 +26547,7 @@ function requireConfirm10(confirm, what) {
24576
26547
  }
24577
26548
  }
24578
26549
  function resolveTextOrFile(value) {
24579
- const text = existsSync(value) ? readFileSync17(value, "utf8") : value;
26550
+ const text = existsSync(value) ? readFileSync18(value, "utf8") : value;
24580
26551
  if (!text.trim()) throw new Error("The text (or the file it points to) is empty.");
24581
26552
  return text.trim();
24582
26553
  }
@@ -24841,7 +26812,7 @@ function registerMediaCommands(program2) {
24841
26812
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
24842
26813
  const result = await client.uploadMediaCaption(mediaId, {
24843
26814
  srclang: options.lang,
24844
- content: readFileSync17(options.file, "utf8"),
26815
+ content: readFileSync18(options.file, "utf8"),
24845
26816
  ...options.label ? { label: options.label } : {}
24846
26817
  });
24847
26818
  emit(flags, result, () => {
@@ -25055,13 +27026,13 @@ function registerMediaCommands(program2) {
25055
27026
  }
25056
27027
 
25057
27028
  // src/commands/newsletters/index.ts
25058
- import { existsSync as existsSync2, readFileSync as readFileSync18 } from "fs";
27029
+ import { existsSync as existsSync2, readFileSync as readFileSync19 } from "fs";
25059
27030
  var ISSUE_STATUSES = ["public", "hidden"];
25060
27031
  var ISSUE_VISIBILITIES = ["public", "hidden", "locked"];
25061
27032
  var ISSUE_RENDER_MODES = ["snapshot", "article"];
25062
27033
  var THUMBNAIL_NONE = "none";
25063
27034
  function resolveMarkdownOrFile(value) {
25064
- return existsSync2(value) ? readFileSync18(value, "utf8") : value;
27035
+ return existsSync2(value) ? readFileSync19(value, "utf8") : value;
25065
27036
  }
25066
27037
  function parseIdList2(value, flag) {
25067
27038
  const ids = value.split(",").map((id) => id.trim()).filter(Boolean);
@@ -25075,7 +27046,7 @@ var ACCESS_STRATEGIES = [
25075
27046
  ];
25076
27047
  var DOUBLE_OPT_INS = ["none", "external"];
25077
27048
  var DELIVERIES = ["zanfia", "integration"];
25078
- function assertOneOf2(value, allowed, flag) {
27049
+ function assertOneOf3(value, allowed, flag) {
25079
27050
  if (!allowed.includes(value)) {
25080
27051
  throw new Error(`Invalid ${flag}: ${value}. Use one of: ${allowed.join(", ")}`);
25081
27052
  }
@@ -25103,7 +27074,7 @@ function parseBool2(value, flag) {
25103
27074
  }
25104
27075
  function parseJsonFileOrInline5(value, flag) {
25105
27076
  const trimmed = value.trim();
25106
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync18(value, "utf8");
27077
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync19(value, "utf8");
25107
27078
  try {
25108
27079
  return JSON.parse(raw);
25109
27080
  } catch (error) {
@@ -25171,17 +27142,17 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
25171
27142
  const flags = getGlobalFlags(command);
25172
27143
  const input = {};
25173
27144
  if (options.status !== void 0) {
25174
- input.status = assertOneOf2(options.status, ISSUE_STATUSES, "--status");
27145
+ input.status = assertOneOf3(options.status, ISSUE_STATUSES, "--status");
25175
27146
  }
25176
27147
  if (options.landing !== void 0) {
25177
- input.landingPageVisibility = assertOneOf2(
27148
+ input.landingPageVisibility = assertOneOf3(
25178
27149
  options.landing,
25179
27150
  ISSUE_VISIBILITIES,
25180
27151
  "--landing"
25181
27152
  );
25182
27153
  }
25183
27154
  if (options.renderMode !== void 0) {
25184
- input.renderMode = assertOneOf2(options.renderMode, ISSUE_RENDER_MODES, "--render-mode");
27155
+ input.renderMode = assertOneOf3(options.renderMode, ISSUE_RENDER_MODES, "--render-mode");
25185
27156
  }
25186
27157
  if (options.thumbnail !== void 0) {
25187
27158
  input.thumbnailUrl = options.thumbnail === THUMBNAIL_NONE ? null : options.thumbnail;
@@ -25460,7 +27431,7 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
25460
27431
  archive.enabled = parseBool2(options.archiveEnabled, "--archive-enabled");
25461
27432
  }
25462
27433
  if (options.archiveAccess !== void 0) {
25463
- archive.accessStrategy = assertOneOf2(
27434
+ archive.accessStrategy = assertOneOf3(
25464
27435
  options.archiveAccess,
25465
27436
  ACCESS_STRATEGIES,
25466
27437
  "--archive-access"
@@ -25470,14 +27441,14 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
25470
27441
  archive.publicationDelay = options.publicationDelay === "none" ? null : parseNonNegativeInt2(options.publicationDelay, "--publication-delay");
25471
27442
  }
25472
27443
  if (options.defaultStatus !== void 0) {
25473
- archive.defaultEmailStatus = assertOneOf2(
27444
+ archive.defaultEmailStatus = assertOneOf3(
25474
27445
  options.defaultStatus,
25475
27446
  ISSUE_STATUSES,
25476
27447
  "--default-status"
25477
27448
  );
25478
27449
  }
25479
27450
  if (options.defaultVisibility !== void 0) {
25480
- archive.defaultEmailVisibility = assertOneOf2(
27451
+ archive.defaultEmailVisibility = assertOneOf3(
25481
27452
  options.defaultVisibility,
25482
27453
  ISSUE_VISIBILITIES,
25483
27454
  "--default-visibility"
@@ -25495,11 +27466,11 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
25495
27466
  input.archiveInbox = inbox;
25496
27467
  }
25497
27468
  if (options.doubleOptIn !== void 0) {
25498
- input.doubleOptIn = assertOneOf2(options.doubleOptIn, DOUBLE_OPT_INS, "--double-opt-in");
27469
+ input.doubleOptIn = assertOneOf3(options.doubleOptIn, DOUBLE_OPT_INS, "--double-opt-in");
25499
27470
  }
25500
27471
  if (options.clientTag.length > 0) input.clientTags = options.clientTag;
25501
27472
  if (options.delivery !== void 0) {
25502
- input.delivery = assertOneOf2(options.delivery, DELIVERIES, "--delivery");
27473
+ input.delivery = assertOneOf3(options.delivery, DELIVERIES, "--delivery");
25503
27474
  }
25504
27475
  if (options.clearSender) {
25505
27476
  input.sender = null;
@@ -25525,7 +27496,7 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
25525
27496
  }
25526
27497
 
25527
27498
  // src/commands/offers/index.ts
25528
- import { readFileSync as readFileSync19 } from "fs";
27499
+ import { readFileSync as readFileSync20 } from "fs";
25529
27500
  var PRICE_BILLING_TYPES = ["one_time", "recurring", "installments"];
25530
27501
  var PRICE_INTERVALS = ["day", "week", "month", "year"];
25531
27502
  var SALE_START_DISPLAYS = ["hidden", "countdown"];
@@ -25562,7 +27533,7 @@ function parseBool3(value, flag) {
25562
27533
  }
25563
27534
  function parseJsonFileOrInline6(value, flag) {
25564
27535
  const trimmed = value.trim();
25565
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync19(value, "utf8");
27536
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync20(value, "utf8");
25566
27537
  try {
25567
27538
  return JSON.parse(raw);
25568
27539
  } catch (error) {
@@ -26073,7 +28044,7 @@ Media gallery (${gallery.length}):
26073
28044
  "suppress subscription-cancelled emails"
26074
28045
  ).option("--disable-end-subscription-emails <on|off>", "suppress subscription-ended emails").option(
26075
28046
  "--disable-access-ending-reminders <on|off>",
26076
- "suppress access-ending reminder emails to the client"
28047
+ "suppress the client's access-end emails: reminders before the end and the e-mail after it (cancel-at-end offers too)"
26077
28048
  ).option("--facebook-pixel-id <id|none>", "legacy Facebook Pixel id (none = clear)").option("--google-analytics-id <id|none>", "legacy GA id (none = clear)").option("--gtm-id <id|none>", "legacy Google Tag Manager id (none = clear)").option("--meta-conversion-integration-id <id|none>", "legacy Meta CAPI id (none = clear)").option(
26078
28049
  "--tracking-integration-ids <ids|none>",
26079
28050
  "comma-separated workspace conversion-tracking integration ids (none = clear)"
@@ -26132,6 +28103,28 @@ Media gallery (${gallery.length}):
26132
28103
  });
26133
28104
  }
26134
28105
  );
28106
+ offers.command("notify-waitlist").description(
28107
+ "\u201EIt's back\u201D: e-mail the offer's waiting list (fr-0272), once per sign-up. Without --send it only counts; --send needs --confirm"
28108
+ ).argument("<checkoutId>", "offer (checkout) id").option("--send", "send the e-mails (default: count only)").option("--confirm", "required with --send \u2014 acknowledge the waiting list is e-mailed").action(
28109
+ async (checkoutId, options, command) => {
28110
+ const flags = getGlobalFlags(command);
28111
+ const send = options.send === true;
28112
+ if (send) requireConfirm(options.confirm, "e-mail this offer's waiting list");
28113
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
28114
+ const result = await client.notifyCheckoutWaitlist(checkoutId, !send);
28115
+ emit(flags, result, () => {
28116
+ process.stdout.write(
28117
+ result.dryRun ? `${result.pendingCount} sign-up(s) of "${result.offerName}" would get the e-mail (nothing sent; add --send --confirm).
28118
+ ` : `Sent to ${result.sentCount} sign-up(s) of "${result.offerName}".
28119
+ `
28120
+ );
28121
+ process.stdout.write(
28122
+ `Already notified: ${result.alreadyNotifiedCount}; skipped: ${result.skippedCount} (suppressed ${result.skippedByReason.suppressed}, unsubscribed after sign-up ${result.skippedByReason.unsubscribed_after_sign_up}).
28123
+ `
28124
+ );
28125
+ });
28126
+ }
28127
+ );
26135
28128
  offers.command("set-default").description("Make an offer its product's default checkout").argument("<checkoutId>", "offer (checkout) id").action(async (checkoutId, _, command) => {
26136
28129
  const flags = getGlobalFlags(command);
26137
28130
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -26368,6 +28361,7 @@ Media gallery (${gallery.length}):
26368
28361
  // src/commands/orders/index.ts
26369
28362
  import { writeFileSync as writeFileSync5 } from "fs";
26370
28363
  var ORDER_STATUSES = ["paid", "pending", "refunded", "failed"];
28364
+ var REFUND_NOTE_MAX_LENGTH = 500;
26371
28365
  function assertOrderStatus(status) {
26372
28366
  if (!ORDER_STATUSES.includes(status)) {
26373
28367
  throw new Error(`Invalid --status: ${status}. Use one of: ${ORDER_STATUSES.join(", ")}`);
@@ -26384,14 +28378,58 @@ function parsePositiveInt13(value, flag) {
26384
28378
  function formatAmount3(amountCents, currency) {
26385
28379
  return `${(amountCents / 100).toFixed(2)} ${currency}`;
26386
28380
  }
28381
+ function formatProducts(order) {
28382
+ const others = (order.productIds ?? []).filter((id) => id !== order.productId).length;
28383
+ return others > 0 ? `${order.productId} +${others}` : order.productId;
28384
+ }
28385
+ function formatLine(line) {
28386
+ const variant = line.variantLabel ? ` (${line.variantLabel})` : "";
28387
+ const discount = line.discountCents > 0 ? ` (\u2212${formatAmount3(line.discountCents, line.currency)})` : "";
28388
+ return ` - ${line.productName}${variant} (${line.productId}) \u2014 ${line.quantity} \xD7 ${formatAmount3(line.unitAmountCents, line.currency)} = ${formatAmount3(line.amountCents, line.currency)}${discount}
28389
+ `;
28390
+ }
28391
+ function writeOrderLines(order) {
28392
+ const lines = order.lineItems ?? [];
28393
+ if (lines.length > 1 || order.origin === "shopCart") {
28394
+ process.stdout.write(` Lines:
28395
+ `);
28396
+ for (const line of lines) process.stdout.write(formatLine(line));
28397
+ } else if (order.additionalProducts.length > 0) {
28398
+ process.stdout.write(` Order bumps:
28399
+ `);
28400
+ for (const bump of order.additionalProducts) {
28401
+ const quantity = bump.quantity > 1 ? `${bump.quantity} \xD7 ` : "";
28402
+ process.stdout.write(
28403
+ ` - ${bump.productName} (${bump.productId}) \u2014 ${quantity}${formatAmount3(bump.amountCents, bump.currency)}
28404
+ `
28405
+ );
28406
+ }
28407
+ }
28408
+ if (order.shipping) {
28409
+ const { shipping } = order;
28410
+ const destination = shipping.lockerPointId ? `locker ${shipping.lockerPointId}` : shipping.address ? `${shipping.address.street}, ${shipping.address.postalCode} ${shipping.address.city}` : "";
28411
+ process.stdout.write(
28412
+ ` Delivery: ${shipping.methodLabel} \u2014 ${formatAmount3(shipping.amountCents, shipping.currency)}${destination ? `, ${destination}` : ""} (${shipping.recipientName})
28413
+ `
28414
+ );
28415
+ }
28416
+ if (order.fulfillment) {
28417
+ const { fulfillment } = order;
28418
+ const tracking = fulfillment.trackingNumber ? `, tracking ${fulfillment.trackingNumber}` : "";
28419
+ process.stdout.write(` Fulfilment: ${fulfillment.status}${tracking}
28420
+ `);
28421
+ }
28422
+ }
26387
28423
  function requireConfirm11(confirm, what) {
26388
28424
  if (!confirm) {
26389
28425
  throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
26390
28426
  }
26391
28427
  }
26392
- function registerOrdersCommands(program2) {
26393
- const orders2 = program2.command("orders").description("Browse orders; approve or reject manual (wire-transfer) payments");
26394
- orders2.command("list").description("List orders (newest first), with optional filters").option("--product <id>", "only orders of this product").option(
28428
+ function registerOrdersCommands2(program2) {
28429
+ const orders2 = program2.command("orders").description(
28430
+ "Browse orders; approve or reject manual (wire-transfer) payments; mark test / refunded orders"
28431
+ );
28432
+ orders2.command("list").description("List orders (newest first), with optional filters").option("--product <id>", "only orders that sold this product (on any line)").option(
26395
28433
  "--status <status>",
26396
28434
  "filter by status (paid, pending, refunded, failed) \u2014 manual wire-transfer orders awaiting approval are `pending`"
26397
28435
  ).option("--from <date>", "only orders created on/after this ISO date").option("--to <date>", "only orders created on/before this ISO date").option("--search <query>", "match client email / name").option("--include-test", "include simulated test purchases (excluded by default)").option("--limit <n>", "page size (max 100)").option("--cursor <cursor>", "keyset cursor from a previous page").action(
@@ -26412,7 +28450,7 @@ function registerOrdersCommands(program2) {
26412
28450
  printTable(
26413
28451
  page.items.map((o) => ({
26414
28452
  id: o.id,
26415
- product: o.productId,
28453
+ product: formatProducts(o),
26416
28454
  client: o.clientEmail,
26417
28455
  amount: formatAmount3(o.amountCents, o.currency),
26418
28456
  status: o.status,
@@ -26428,7 +28466,7 @@ Next page: --cursor ${page.nextCursor}
26428
28466
  });
26429
28467
  }
26430
28468
  );
26431
- orders2.command("get").description("Show a single order (payment, refund, order bumps, invoice)").argument("<orderId>", "order id").action(async (orderId, _, command) => {
28469
+ orders2.command("get").description("Show a single order (payment, refund, lines or order bumps, delivery, invoice)").argument("<orderId>", "order id").action(async (orderId, _, command) => {
26432
28470
  const flags = getGlobalFlags(command);
26433
28471
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26434
28472
  const order = await client.getOrder(orderId);
@@ -26463,30 +28501,24 @@ Next page: --cursor ${page.nextCursor}
26463
28501
  `
26464
28502
  );
26465
28503
  }
26466
- if (order.additionalProducts.length > 0) {
26467
- process.stdout.write(` Order bumps:
26468
- `);
26469
- for (const bump of order.additionalProducts) {
26470
- const quantity = bump.quantity > 1 ? `${bump.quantity} \xD7 ` : "";
26471
- process.stdout.write(
26472
- ` - ${bump.productName} (${bump.productId}) \u2014 ${quantity}${formatAmount3(bump.amountCents, bump.currency)}
26473
- `
26474
- );
26475
- }
26476
- }
28504
+ writeOrderLines(order);
26477
28505
  if (order.invoiceUrl) process.stdout.write(` Invoice: ${order.invoiceUrl}
26478
28506
  `);
26479
28507
  });
26480
28508
  });
26481
28509
  orders2.command("approve").description(
26482
- "Approve a manual (wire-transfer) order \u2014 grants product access and sends the regular purchase email; --issue-invoice also issues an invoice via the connected invoicing account. 409 = the order was already decided. Requires --confirm"
26483
- ).argument("<orderId>", "order id (a `pending` order from `orders list --status pending`)").option("--issue-invoice", "also issue an invoice for the order (never issued without this)").option("--confirm", "required \u2014 acknowledge that access is granted and the buyer is emailed").action(
28510
+ "Approve a manual (wire-transfer) order \u2014 grants product access and sends the regular purchase email; --issue-invoice also issues an invoice via the connected invoicing account. 409 = the order was already decided, or (stock-exhausted) an expired physical order whose stock ran out: re-run with --allow-oversell or reject it. Requires --confirm"
28511
+ ).argument("<orderId>", "order id (a `pending` order from `orders list --status pending`)").option("--issue-invoice", "also issue an invoice for the order (never issued without this)").option(
28512
+ "--allow-oversell",
28513
+ "approve an expired physical order (late transfer) even though its stock ran out"
28514
+ ).option("--confirm", "required \u2014 acknowledge that access is granted and the buyer is emailed").action(
26484
28515
  async (orderId, options, command) => {
26485
28516
  const flags = getGlobalFlags(command);
26486
28517
  requireConfirm11(options.confirm, `approve order ${orderId} (access + purchase email)`);
26487
28518
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26488
28519
  const result = await client.approveManualOrder(orderId, {
26489
- issueInvoice: options.issueInvoice === true ? true : void 0
28520
+ issueInvoice: options.issueInvoice === true ? true : void 0,
28521
+ allowOversell: options.allowOversell === true ? true : void 0
26490
28522
  });
26491
28523
  emit(flags, result, () => {
26492
28524
  process.stdout.write(
@@ -26508,9 +28540,69 @@ Next page: --cursor ${page.nextCursor}
26508
28540
  `);
26509
28541
  });
26510
28542
  });
28543
+ orders2.command("mark-test").description(
28544
+ "Mark a real order as a test \u2014 it leaves revenue, charts and reports; the buyer keeps access. --undo puts it back. 409 = already in that state. Requires --confirm"
28545
+ ).argument("<orderId>", "order id").option("--undo", "undo a mark made this way (the order counts again)").option("--confirm", "required \u2014 acknowledge the change to revenue and reports").action(
28546
+ async (orderId, options, command) => {
28547
+ const flags = getGlobalFlags(command);
28548
+ const isTest = options.undo !== true;
28549
+ requireConfirm11(
28550
+ options.confirm,
28551
+ isTest ? `mark order ${orderId} as a test` : `undo the test mark of order ${orderId}`
28552
+ );
28553
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
28554
+ const result = await client.markOrderAsTest(orderId, isTest);
28555
+ emit(flags, result, () => {
28556
+ process.stdout.write(
28557
+ result.isTest ? `Order ${result.orderId} is marked as a test \u2014 it no longer counts in revenue.
28558
+ ` : `Order ${result.orderId} counts again in revenue and reports.
28559
+ `
28560
+ );
28561
+ });
28562
+ }
28563
+ );
28564
+ orders2.command("mark-refunded").description(
28565
+ "Record that you returned the money OUTSIDE Zanfia (bank transfer, cash\u2026). Nothing is charged or paid out and no correcting invoice is issued (do that in your invoicing tool); the order counts as refunded. --revoke-access also ends the buyer\u2019s access (refused while a live subscription would keep billing). Requires --confirm"
28566
+ ).argument("<orderId>", "order id").option("--revoke-access", "also end the buyer\u2019s access to the products of this order").option(
28567
+ "--note <text>",
28568
+ `note on the order's audit trail (max ${REFUND_NOTE_MAX_LENGTH} chars)`
28569
+ ).option("--confirm", "required \u2014 acknowledge that the order counts as refunded").action(
28570
+ async (orderId, options, command) => {
28571
+ const flags = getGlobalFlags(command);
28572
+ if (options.note !== void 0 && options.note.length > REFUND_NOTE_MAX_LENGTH) {
28573
+ throw new Error(`--note is too long (max ${REFUND_NOTE_MAX_LENGTH} characters).`);
28574
+ }
28575
+ const revokeAccess = options.revokeAccess === true;
28576
+ requireConfirm11(
28577
+ options.confirm,
28578
+ `mark order ${orderId} as refunded outside Zanfia${revokeAccess ? " and end the buyer\u2019s access" : ""}`
28579
+ );
28580
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
28581
+ const result = await client.markOrderRefunded(orderId, {
28582
+ revokeAccess,
28583
+ ...options.note !== void 0 ? { note: options.note } : {}
28584
+ });
28585
+ emit(flags, result, () => {
28586
+ process.stdout.write(
28587
+ `Order ${result.orderId} recorded as refunded outside Zanfia (${formatAmount3(result.amountMinor, result.currency)}).
28588
+ `
28589
+ );
28590
+ if (result.revokedProductIds.length > 0) {
28591
+ process.stdout.write(` Access ended: ${result.revokedProductIds.join(", ")}
28592
+ `);
28593
+ }
28594
+ if (result.accessRevokeFailedProductIds.length > 0) {
28595
+ process.stdout.write(
28596
+ ` \u26A0\uFE0F Access NOT ended for: ${result.accessRevokeFailedProductIds.join(", ")} \u2014 the refund stands; end it on the client card.
28597
+ `
28598
+ );
28599
+ }
28600
+ });
28601
+ }
28602
+ );
26511
28603
  orders2.command("export").description(
26512
28604
  "Export orders as CSV to a file. The --from/--to window is required; same filters as `orders list`."
26513
- ).requiredOption("--from <date>", "inclusive ISO date lower bound on createdAt (required)").requiredOption("--to <date>", "inclusive ISO date upper bound on createdAt (required)").option("--product <id>", "only orders of this product").option("--status <status>", "filter by status (paid, pending, refunded, failed)").option("--search <query>", "match client email / name").option("--include-test", "include simulated test purchases (excluded by default)").option("--max-rows <n>", "row cap (server default 10000, max 20000)").option("--out <file>", "output file path", "zanfia-orders.csv").action(
28605
+ ).requiredOption("--from <date>", "inclusive ISO date lower bound on createdAt (required)").requiredOption("--to <date>", "inclusive ISO date upper bound on createdAt (required)").option("--product <id>", "only orders that sold this product (on any line)").option("--status <status>", "filter by status (paid, pending, refunded, failed)").option("--search <query>", "match client email / name").option("--include-test", "include simulated test purchases (excluded by default)").option("--max-rows <n>", "row cap (server default 10000, max 20000)").option("--out <file>", "output file path", "zanfia-orders.csv").action(
26514
28606
  async (options, command) => {
26515
28607
  const flags = getGlobalFlags(command);
26516
28608
  for (const [flag, value] of [
@@ -26550,12 +28642,12 @@ Next page: --cursor ${page.nextCursor}
26550
28642
  }
26551
28643
 
26552
28644
  // src/commands/oto/index.ts
26553
- import { readFileSync as readFileSync20 } from "fs";
28645
+ import { readFileSync as readFileSync21 } from "fs";
26554
28646
  var MIN_TIMER_MINUTES = 1;
26555
28647
  var MAX_TIMER_MINUTES = 120;
26556
28648
  function parseJsonFileOrInline7(value, flag) {
26557
28649
  const trimmed = value.trim();
26558
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync20(value, "utf8");
28650
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync21(value, "utf8");
26559
28651
  try {
26560
28652
  return JSON.parse(raw);
26561
28653
  } catch (error) {
@@ -26743,7 +28835,7 @@ function registerOtoCommands(program2) {
26743
28835
  }
26744
28836
 
26745
28837
  // src/commands/pages/index.ts
26746
- import { existsSync as existsSync3, readFileSync as readFileSync21 } from "fs";
28838
+ import { existsSync as existsSync3, readFileSync as readFileSync22 } from "fs";
26747
28839
  import { readFile as readFile4 } from "fs/promises";
26748
28840
  var CLEAR_VALUE = "none";
26749
28841
  var FAMILY_STEPS = ["follow-ups", "thank-you-page", "room-page"];
@@ -26768,6 +28860,14 @@ function buildEventInput(options) {
26768
28860
  };
26769
28861
  return Object.keys(event).length > 0 ? event : void 0;
26770
28862
  }
28863
+ function pointPlaceholderRefusalAtFlag(hint) {
28864
+ return (error) => {
28865
+ if (error instanceof ApiError && error.status === 409 && error.apiMessage.includes("allowPlaceholders")) {
28866
+ throw new ApiError(error.status, error.apiMessage, hint);
28867
+ }
28868
+ throw error;
28869
+ };
28870
+ }
26771
28871
  async function resolveHtml2(options) {
26772
28872
  if (options.file) {
26773
28873
  return readFile4(options.file, "utf8");
@@ -26814,13 +28914,13 @@ function parseCsv4(value) {
26814
28914
  ];
26815
28915
  }
26816
28916
  function resolveTextOrFile2(value) {
26817
- const text = existsSync3(value) ? readFileSync21(value, "utf8") : value;
28917
+ const text = existsSync3(value) ? readFileSync22(value, "utf8") : value;
26818
28918
  if (!text.trim()) throw new Error("The text (or the file it points to) is empty.");
26819
28919
  return text;
26820
28920
  }
26821
28921
  function parseJsonObjectFileOrInline3(value, flag) {
26822
28922
  const trimmed = value.trim();
26823
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync21(value, "utf8");
28923
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync22(value, "utf8");
26824
28924
  let parsed;
26825
28925
  try {
26826
28926
  parsed = JSON.parse(raw);
@@ -26934,7 +29034,10 @@ function registerPagesCommands(program2) {
26934
29034
  );
26935
29035
  });
26936
29036
  });
26937
- pages.command("create").description("Create a hosted page from an HTML file or inline HTML").requiredOption("--slug <slug>", "public URL slug (served at /page/<slug>)").requiredOption("--title <title>", "page title").option("--file <path>", "path to an .html file to host").option("--html <html>", "inline HTML body (alternative to --file)").option("--publish", "publish immediately instead of saving as draft").action(async (options, command) => {
29037
+ pages.command("create").description("Create a hosted page from an HTML file or inline HTML").requiredOption("--slug <slug>", "public URL slug (served at /page/<slug>)").requiredOption("--title <title>", "page title").option("--file <path>", "path to an .html file to host").option("--html <html>", "inline HTML body (alternative to --file)").option("--publish", "publish immediately instead of saving as draft").option(
29038
+ "--allow-placeholders",
29039
+ "with --publish: publish even though the page still shows [UZUPE\u0141NIJ: \u2026] placeholders"
29040
+ ).action(async (options, command) => {
26938
29041
  const flags = getGlobalFlags(command);
26939
29042
  const html = await resolveHtml2(options);
26940
29043
  if (!html) {
@@ -26944,10 +29047,15 @@ function registerPagesCommands(program2) {
26944
29047
  slug: options.slug,
26945
29048
  title: options.title,
26946
29049
  html,
26947
- publish: options.publish ?? false
29050
+ publish: options.publish ?? false,
29051
+ ...options.allowPlaceholders ? { allowPlaceholders: true } : {}
26948
29052
  };
26949
29053
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26950
- const result = await client.createPage(input);
29054
+ const result = await client.createPage(input).catch(
29055
+ pointPlaceholderRefusalAtFlag(
29056
+ "Fill them in, create it as a draft (without --publish), or re-run with --allow-placeholders to publish anyway."
29057
+ )
29058
+ );
26951
29059
  emit(flags, result, () => {
26952
29060
  process.stdout.write(`Created page ${result.pageId} \u2014 /${result.slug}
26953
29061
  `);
@@ -26997,14 +29105,26 @@ function registerPagesCommands(program2) {
26997
29105
  });
26998
29106
  });
26999
29107
  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(
29108
+ const verbCommand = 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");
29109
+ if (publish) {
29110
+ verbCommand.option(
29111
+ "--allow-placeholders",
29112
+ "publish even though pages still show [UZUPE\u0141NIJ: \u2026] placeholders"
29113
+ );
29114
+ }
29115
+ verbCommand.action(
27001
29116
  async (id, options, command) => {
27002
29117
  const flags = getGlobalFlags(command);
27003
29118
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27004
29119
  const result = await client.publishPage(id, publish, {
27005
29120
  ...options.onlyThisPage ? { scope: "page" } : {},
27006
- ...options.dryRun ? { dryRun: true } : {}
27007
- });
29121
+ ...options.dryRun ? { dryRun: true } : {},
29122
+ ...options.allowPlaceholders ? { allowPlaceholders: true } : {}
29123
+ }).catch(
29124
+ pointPlaceholderRefusalAtFlag(
29125
+ "Fill them in (pages update), or re-run with --allow-placeholders to publish anyway."
29126
+ )
29127
+ );
27008
29128
  emit(flags, result, () => {
27009
29129
  if (!result.dryRun) {
27010
29130
  process.stdout.write(`Page ${result.pageId} is now ${result.status}
@@ -27165,7 +29285,7 @@ function registerPagesCommands(program2) {
27165
29285
  });
27166
29286
  });
27167
29287
  pages.command("restore").description(
27168
- "Roll a page's HTML back to a revision. The current HTML is snapshotted first (source `restore`), so a restore is itself undoable. Requires --confirm."
29288
+ 'Roll a page\'s HTML back to a revision. The current HTML is snapshotted first (source `restore`), so a restore is itself undoable. On a published page a revision with a buy button that leads nowhere, or with placeholder text the live page does not show ([UZUPE\u0141NIJ: \u2026], a <span class="todo"> note, LINK: \u2026), is refused (409, nothing restored). Requires --confirm.'
27169
29289
  ).argument("<pageId>", "page id").argument("<revisionId>", "revision id from `pages revisions`").option("--confirm", "required \u2014 acknowledge that the current HTML will be replaced").action(
27170
29290
  async (pageId, revisionId, options, command) => {
27171
29291
  const flags = getGlobalFlags(command);
@@ -27226,9 +29346,14 @@ function registerPagesCommands(program2) {
27226
29346
  }
27227
29347
 
27228
29348
  // src/commands/products/index.ts
27229
- import { readFileSync as readFileSync22 } from "fs";
29349
+ import { readFileSync as readFileSync23 } from "fs";
27230
29350
  import { readFile as readFile5 } from "fs/promises";
27231
29351
  import { basename as basename2 } from "path";
29352
+ function parseBuyerFiles(value) {
29353
+ if (value === "on") return true;
29354
+ if (value === "off") return false;
29355
+ throw new Error(`Invalid --buyer-files: ${value} (expected on | off)`);
29356
+ }
27232
29357
  var KB_TREE_CHANGED_ERROR = "tree-changed";
27233
29358
  var JSON_FLAG_HINT3 = "(write it as --json=<value> \u2014 a bare --json is the global JSON-output flag)";
27234
29359
  var PRODUCT_TYPE_INPUTS = [
@@ -27342,7 +29467,7 @@ function assertIsoDate2(value, flag) {
27342
29467
  }
27343
29468
  return value;
27344
29469
  }
27345
- function assertOneOf3(value, allowed, flag) {
29470
+ function assertOneOf4(value, allowed, flag) {
27346
29471
  if (!allowed.includes(value)) {
27347
29472
  throw new Error(`Invalid ${flag}: ${value}. Use one of: ${allowed.join(", ")}`);
27348
29473
  }
@@ -27350,7 +29475,7 @@ function assertOneOf3(value, allowed, flag) {
27350
29475
  }
27351
29476
  function parseJsonFileOrInline8(value, flag) {
27352
29477
  const trimmed = value.trim();
27353
- const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync22(value, "utf8");
29478
+ const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync23(value, "utf8");
27354
29479
  try {
27355
29480
  return JSON.parse(raw);
27356
29481
  } catch (error) {
@@ -27403,7 +29528,7 @@ function printAddedFiles(inputCount, result) {
27403
29528
  );
27404
29529
  printFilesTable(result.files);
27405
29530
  }
27406
- function registerProductsCommands(program2) {
29531
+ function registerProductsCommands2(program2) {
27407
29532
  const products2 = program2.command("products").description("Manage products");
27408
29533
  products2.command("list").description("List products in the workspace").action(async (_, command) => {
27409
29534
  const flags = getGlobalFlags(command);
@@ -27441,6 +29566,10 @@ function registerProductsCommands(program2) {
27441
29566
  }
27442
29567
  if (product.language !== void 0) {
27443
29568
  process.stdout.write(` Language: ${product.language ?? "none (platform language)"}
29569
+ `);
29570
+ }
29571
+ if (product.fileSetEnabled !== void 0) {
29572
+ process.stdout.write(` Files for buyers: ${product.fileSetEnabled ? "on" : "off"}
27444
29573
  `);
27445
29574
  }
27446
29575
  process.stdout.write(` Clients: ${product.clientCount}
@@ -27478,7 +29607,7 @@ function registerProductsCommands(program2) {
27478
29607
  if (options.description !== void 0 && options.descriptionFile !== void 0) {
27479
29608
  throw new Error("Use either --description or --description-file, not both.");
27480
29609
  }
27481
- const description = options.descriptionFile !== void 0 ? readFileSync22(options.descriptionFile, "utf8") : options.description;
29610
+ const description = options.descriptionFile !== void 0 ? readFileSync23(options.descriptionFile, "utf8") : options.description;
27482
29611
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
27483
29612
  const product = await client.createProduct({
27484
29613
  name: options.name,
@@ -27534,7 +29663,9 @@ function registerProductsCommands(program2) {
27534
29663
  });
27535
29664
  }
27536
29665
  );
27537
- products2.command("update").description("Update a product's name, description, storefront visibility and/or language").argument("<id>", "product id").option("--name <name>", "new product name").option("--description <description>", "new product description").option(
29666
+ products2.command("update").description(
29667
+ "Update a product's name, description, storefront visibility, language and/or files for buyers"
29668
+ ).argument("<id>", "product id").option("--name <name>", "new product name").option("--description <description>", "new product description").option(
27538
29669
  "--visibility <public|unlisted|private>",
27539
29670
  "where buyers find it once published: public (store + product page), unlisted (product page by link only), private (checkout links only)",
27540
29671
  parseStorefrontVisibility
@@ -27542,11 +29673,15 @@ function registerProductsCommands(program2) {
27542
29673
  "--language <pl|en|none>",
27543
29674
  "the product's language (buyer-facing pages and e-mails); none clears it (the platform's language applies)",
27544
29675
  parseProductLanguageOrNone
29676
+ ).option(
29677
+ "--buyer-files <on|off>",
29678
+ "\u201EPliki dla kupuj\u0105cych\u201D: on opens the product's file set (\u201EPliki\u201D, `files push`) to its buyers, off hides it from them (the files stay saved)",
29679
+ parseBuyerFiles
27545
29680
  ).action(
27546
29681
  async (id, options, command) => {
27547
- if (options.name === void 0 && options.description === void 0 && options.visibility === void 0 && options.language === void 0) {
29682
+ if (options.name === void 0 && options.description === void 0 && options.visibility === void 0 && options.language === void 0 && options.buyerFiles === void 0) {
27548
29683
  throw new Error(
27549
- "Nothing to update: pass --name, --description, --visibility and/or --language."
29684
+ "Nothing to update: pass --name, --description, --visibility, --language and/or --buyer-files."
27550
29685
  );
27551
29686
  }
27552
29687
  const flags = getGlobalFlags(command);
@@ -27555,7 +29690,8 @@ function registerProductsCommands(program2) {
27555
29690
  name: options.name,
27556
29691
  description: options.description,
27557
29692
  storefrontVisibility: options.visibility,
27558
- language: options.language === PRODUCT_LANGUAGE_NONE ? null : options.language
29693
+ language: options.language === PRODUCT_LANGUAGE_NONE ? null : options.language,
29694
+ fileSetEnabled: options.buyerFiles
27559
29695
  });
27560
29696
  emit(flags, result, () => {
27561
29697
  process.stdout.write(`Updated product ${result.productId}
@@ -27592,6 +29728,23 @@ function registerProductsCommands(program2) {
27592
29728
  }
27593
29729
  });
27594
29730
  });
29731
+ products2.command("duplicate").description(
29732
+ "Copy a product as a DRAFT (settings, offers with new prices, content) in a background run; community / knowledge base / newsletter are refused (422). Requires --confirm"
29733
+ ).argument("<id>", "product id").option("--name <name>", 'name of the copy (default: "<name> (kopia)" / "(copy)")').option("--confirm", "required \u2014 acknowledge a new draft product is created").action(async (id, options, command) => {
29734
+ requireConfirm(options.confirm, `duplicate product ${id}`);
29735
+ const flags = getGlobalFlags(command);
29736
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
29737
+ const result = await client.duplicateProduct(id, options.name);
29738
+ emit(
29739
+ flags,
29740
+ result,
29741
+ () => process.stdout.write(
29742
+ result.status === "already-running" ? `Product ${id} is already being copied (run ${result.runId}); no second copy.
29743
+ ` : `Copying product ${id}${result.productId ? ` into ${result.productId}` : ""} (run ${result.runId}); the draft appears in \`products list\` when the run completes.
29744
+ `
29745
+ )
29746
+ );
29747
+ });
27595
29748
  products2.command("publish").description("Publish a product (status \u2192 published; live for customers)").argument("<id>", "product id").action(async (id, _, command) => {
27596
29749
  const flags = getGlobalFlags(command);
27597
29750
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -27849,10 +30002,10 @@ function registerProductsCommands(program2) {
27849
30002
  ).option("--delivery-time <n>", "service: delivery time value").option("--delivery-unit <unit>", `service: ${DELIVERY_UNITS.join("|")} (with --delivery-time)`).option("--revision-rounds <n|unlimited>", 'service: revision rounds count or "unlimited"').option("--brief-method <method>", `service: ${BRIEF_METHODS.join("|")}`).option("--brief-url <url>", "service: external brief/intake form URL").option("--event-mode <mode>", `event: ${EVENT_MODES3.join("|")}`).option("--starts <iso>", "event: start (ISO 8601)").option("--ends <iso>", "event: end (ISO 8601)").option("--capacity <n>", "event: attendee capacity").option("--show-on-success <on|off>", "show the post-purchase CTA on the success page").action(
27850
30003
  async (productId, options, command) => {
27851
30004
  const flags = getGlobalFlags(command);
27852
- const bookingMethod = options.bookingMethod !== void 0 ? assertOneOf3(options.bookingMethod, BOOKING_METHODS, "--booking-method") : void 0;
27853
- const briefMethod = options.briefMethod !== void 0 ? assertOneOf3(options.briefMethod, BRIEF_METHODS, "--brief-method") : void 0;
27854
- const deliveryUnit = options.deliveryUnit !== void 0 ? assertOneOf3(options.deliveryUnit, DELIVERY_UNITS, "--delivery-unit") : void 0;
27855
- const eventMode = options.eventMode !== void 0 ? assertOneOf3(options.eventMode, EVENT_MODES3, "--event-mode") : void 0;
30005
+ const bookingMethod = options.bookingMethod !== void 0 ? assertOneOf4(options.bookingMethod, BOOKING_METHODS, "--booking-method") : void 0;
30006
+ const briefMethod = options.briefMethod !== void 0 ? assertOneOf4(options.briefMethod, BRIEF_METHODS, "--brief-method") : void 0;
30007
+ const deliveryUnit = options.deliveryUnit !== void 0 ? assertOneOf4(options.deliveryUnit, DELIVERY_UNITS, "--delivery-unit") : void 0;
30008
+ const eventMode = options.eventMode !== void 0 ? assertOneOf4(options.eventMode, EVENT_MODES3, "--event-mode") : void 0;
27856
30009
  if (options.deliveryTime === void 0 !== (options.deliveryUnit === void 0)) {
27857
30010
  throw new Error("Pass --delivery-time and --delivery-unit together.");
27858
30011
  }
@@ -27984,7 +30137,7 @@ function registerProductsCommands(program2) {
27984
30137
  ).option("--thank-you-type <type>", `${THANK_YOU_TYPES.join("|")}`).option("--redirect-url <url>", "redirect destination (implies --thank-you-type redirect)").option("--email-buyer-purchase <on|off>", "send the buyer a purchase confirmation email").option("--notify-owner-purchase <on|off>", "notify the owner of a purchase").option("--email-buyer-failed <on|off>", "email the buyer on a failed payment").option("--notify-owner-failed <on|off>", "notify the owner of a failed payment").action(
27985
30138
  async (productId, options, command) => {
27986
30139
  const flags = getGlobalFlags(command);
27987
- const thankYouType = options.thankYouType !== void 0 ? assertOneOf3(options.thankYouType, THANK_YOU_TYPES, "--thank-you-type") : void 0;
30140
+ const thankYouType = options.thankYouType !== void 0 ? assertOneOf4(options.thankYouType, THANK_YOU_TYPES, "--thank-you-type") : void 0;
27988
30141
  if (options.redirectUrl !== void 0)
27989
30142
  assertHttpsUrl(options.redirectUrl, "--redirect-url");
27990
30143
  const emailBuyerPurchase = options.emailBuyerPurchase !== void 0 ? parseBool4(options.emailBuyerPurchase, "--email-buyer-purchase") : void 0;
@@ -28077,7 +30230,7 @@ function registerProductsCommands(program2) {
28077
30230
  if (options.content !== void 0 && options.contentFile !== void 0) {
28078
30231
  throw new Error("Use either --content or --content-file, not both.");
28079
30232
  }
28080
- const editorContent = options.contentFile !== void 0 ? readFileSync22(options.contentFile, "utf8") : options.content;
30233
+ const editorContent = options.contentFile !== void 0 ? readFileSync23(options.contentFile, "utf8") : options.content;
28081
30234
  if (editorContent === void 0) {
28082
30235
  throw new Error("Pass the description via --content or --content-file.");
28083
30236
  }
@@ -28183,7 +30336,7 @@ function registerProductsCommands(program2) {
28183
30336
  if (options.content !== void 0 && options.contentFile !== void 0) {
28184
30337
  throw new Error("Use either --content or --content-file, not both.");
28185
30338
  }
28186
- return options.contentFile !== void 0 ? readFileSync22(options.contentFile, "utf8") : options.content;
30339
+ return options.contentFile !== void 0 ? readFileSync23(options.contentFile, "utf8") : options.content;
28187
30340
  };
28188
30341
  kb.command("get").description(
28189
30342
  "Show the knowledge base: categories (tree keys = the --category ids) + all articles (drafts and unavailable included \u2014 the creator view)"
@@ -28282,7 +30435,7 @@ function registerProductsCommands(program2) {
28282
30435
  title: options.title,
28283
30436
  parentId: options.category,
28284
30437
  editorContent: resolveKbContent(options),
28285
- status: options.status !== void 0 ? assertOneOf3(options.status, KB_ARTICLE_STATUSES, "--status") : void 0,
30438
+ status: options.status !== void 0 ? assertOneOf4(options.status, KB_ARTICLE_STATUSES, "--status") : void 0,
28286
30439
  tags: options.tags !== void 0 ? parseKbTags(options.tags) : void 0
28287
30440
  });
28288
30441
  emit(flags, result, () => {
@@ -28320,7 +30473,7 @@ function registerProductsCommands(program2) {
28320
30473
  const result = await client.updateKbArticle(productId, articleId, {
28321
30474
  title: options.title,
28322
30475
  editorContent,
28323
- status: options.status !== void 0 ? assertOneOf3(options.status, KB_ARTICLE_STATUSES, "--status") : void 0,
30476
+ status: options.status !== void 0 ? assertOneOf4(options.status, KB_ARTICLE_STATUSES, "--status") : void 0,
28324
30477
  parentId: options.category,
28325
30478
  coverImage,
28326
30479
  tags
@@ -28403,7 +30556,7 @@ function registerProductsCommands(program2) {
28403
30556
  }
28404
30557
 
28405
30558
  // src/commands/referrals/index.ts
28406
- import { readFileSync as readFileSync23 } from "fs";
30559
+ import { readFileSync as readFileSync24 } from "fs";
28407
30560
  var PROGRAM_KINDS = ["referral", "affiliate"];
28408
30561
  var WRITABLE_STATUSES = ["active", "inactive"];
28409
30562
  var ENROLLMENT_MODES = ["open", "inviteOnly", "closed"];
@@ -28432,7 +30585,7 @@ function parseIdList3(value, flag) {
28432
30585
  }
28433
30586
  function parseJsonObjectFileOrInline4(value, flag) {
28434
30587
  const trimmed = value.trim();
28435
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync23(value, "utf8");
30588
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync24(value, "utf8");
28436
30589
  let parsed;
28437
30590
  try {
28438
30591
  parsed = JSON.parse(raw);
@@ -28799,7 +30952,7 @@ Failed deliveries (${result.failedDeliveries.length})
28799
30952
  }
28800
30953
 
28801
30954
  // src/commands/segments/index.ts
28802
- import { readFileSync as readFileSync24 } from "fs";
30955
+ import { readFileSync as readFileSync25 } from "fs";
28803
30956
  var SUBSCRIPTION_STATUSES2 = ["pending", "subscribed", "unsubscribed"];
28804
30957
  function collectRepeatable7(value, previous) {
28805
30958
  return [...previous, value];
@@ -28818,7 +30971,7 @@ function resolveConditions(options) {
28818
30971
  if (hasFlagConditions) {
28819
30972
  throw new Error("Use either condition flags or --json-file, not both.");
28820
30973
  }
28821
- const raw = readFileSync24(options.jsonFile, "utf8");
30974
+ const raw = readFileSync25(options.jsonFile, "utf8");
28822
30975
  let parsed;
28823
30976
  try {
28824
30977
  parsed = JSON.parse(raw);
@@ -29051,59 +31204,224 @@ function registerSegmentsCommands(program2) {
29051
31204
  ).argument("<id>", "segment id").option("--name <name>", "new segment name")
29052
31205
  ).action(async (id, options, command) => {
29053
31206
  const flags = getGlobalFlags(command);
29054
- const input = {};
29055
- if (options.name !== void 0) input.name = options.name;
29056
- const conditions = resolveConditions(options);
29057
- if (conditions !== void 0) input.conditions = conditions;
29058
- if (Object.keys(input).length === 0) {
29059
- throw new Error(
29060
- `Nothing to update \u2014 pass --name and/or conditions (${CONDITION_FLAGS_HELP}).`
31207
+ const input = {};
31208
+ if (options.name !== void 0) input.name = options.name;
31209
+ const conditions = resolveConditions(options);
31210
+ if (conditions !== void 0) input.conditions = conditions;
31211
+ if (Object.keys(input).length === 0) {
31212
+ throw new Error(
31213
+ `Nothing to update \u2014 pass --name and/or conditions (${CONDITION_FLAGS_HELP}).`
31214
+ );
31215
+ }
31216
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
31217
+ const result = await client.updateAudienceSegment(id, input);
31218
+ emit(flags, result, () => {
31219
+ const { segment } = result;
31220
+ process.stdout.write(
31221
+ `Updated segment ${segment.id} (${segment.name})${segment.lastEvaluatedCount !== null ? ` \u2014 ~${segment.lastEvaluatedCount} clients match` : ""}.
31222
+ `
31223
+ );
31224
+ });
31225
+ });
31226
+ segments.command("delete").description(
31227
+ "Delete a segment \u2014 campaigns already sent to it are unaffected; drafts targeting it will fail to send until re-pointed"
31228
+ ).argument("<id>", "segment id").action(async (id, _options, command) => {
31229
+ const flags = getGlobalFlags(command);
31230
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
31231
+ const result = await client.deleteAudienceSegment(id);
31232
+ emit(flags, result, () => {
31233
+ process.stdout.write(`Deleted segment ${result.segmentId}.
31234
+ `);
31235
+ });
31236
+ });
31237
+ withConditionOptions(
31238
+ segments.command("preview").description(
31239
+ "Count the clients matching a condition set WITHOUT saving anything (stateless dry run)"
31240
+ ).option(
31241
+ "--email-sendable",
31242
+ "count only clients an e-mail campaign may mail (subscribed): the reach of a send"
31243
+ ).option(
31244
+ "--sms-sendable",
31245
+ "count only clients an SMS campaign may text (SMS consent AND a phone): size an SMS send"
31246
+ )
31247
+ ).action(async (options, command) => {
31248
+ const flags = getGlobalFlags(command);
31249
+ const conditions = resolveConditions(options);
31250
+ if (conditions === void 0) {
31251
+ throw new Error(`Pass at least one condition: ${CONDITION_FLAGS_HELP}.`);
31252
+ }
31253
+ if (options.emailSendable && options.smsSendable) {
31254
+ throw new Error("Pass either --email-sendable or --sms-sendable, not both.");
31255
+ }
31256
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
31257
+ const result = await client.previewAudienceSegment(conditions, {
31258
+ requireSendable: options.emailSendable === true,
31259
+ requireSmsSendable: options.smsSendable === true
31260
+ });
31261
+ emit(flags, result, () => {
31262
+ const count2 = result.partial ? `\u2265 ${result.count}` : String(result.count);
31263
+ const reach = options.smsSendable ? " (SMS reach)" : options.emailSendable ? " (e-mail reach)" : "";
31264
+ process.stdout.write(
31265
+ `${count2} client${result.count === 1 && !result.partial ? " matches" : "s match"} ${conditions.map(describeCondition).join(" AND ")}${reach}.
31266
+ ` + (result.partial ? "The client base was not counted to the end in time: the real number may be higher.\n" : "")
31267
+ );
31268
+ });
31269
+ });
31270
+ }
31271
+
31272
+ // src/commands/site/index.ts
31273
+ import { existsSync as existsSync4, readFileSync as readFileSync26 } from "fs";
31274
+
31275
+ // src/commands/site/librarySections.ts
31276
+ var NO_SECTION = "none";
31277
+ function isStringArray(value) {
31278
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
31279
+ }
31280
+ function toSectionDrafts(rows) {
31281
+ return rows.map((row, index) => {
31282
+ if (typeof row !== "object" || row === null || Array.isArray(row)) {
31283
+ throw new Error(`--sections[${index}] must be an object {id?, name, productIds}.`);
31284
+ }
31285
+ const name = "name" in row ? row.name : void 0;
31286
+ const productIds = "productIds" in row ? row.productIds : [];
31287
+ const id = "id" in row ? row.id : void 0;
31288
+ if (typeof name !== "string") throw new Error(`--sections[${index}].name must be a string.`);
31289
+ if (!isStringArray(productIds)) {
31290
+ throw new Error(`--sections[${index}].productIds must be an array of product ids.`);
31291
+ }
31292
+ if (id !== void 0 && id !== null && typeof id !== "string") {
31293
+ throw new Error(`--sections[${index}].id must be a section id or null.`);
31294
+ }
31295
+ return { ...typeof id === "string" ? { id } : {}, name, productIds };
31296
+ });
31297
+ }
31298
+ function printSectionsView(result) {
31299
+ printTable(
31300
+ result.sections.map((section) => ({
31301
+ id: section.id,
31302
+ name: section.name,
31303
+ products: section.products.map((product) => product.name).join(", ")
31304
+ })),
31305
+ ["id", "name", "products"]
31306
+ );
31307
+ if (result.unassignedProducts.length > 0) {
31308
+ process.stdout.write(
31309
+ `
31310
+ Pozosta\u0142e (no section): ${result.unassignedProducts.map((product) => product.name).join(", ")}
31311
+ `
31312
+ );
31313
+ }
31314
+ }
31315
+ function printStoredSections(result, done) {
31316
+ process.stdout.write(`${done}
31317
+ `);
31318
+ printTable(
31319
+ result.sections.map((section) => ({
31320
+ id: section.id,
31321
+ name: section.name,
31322
+ products: section.productIds.length
31323
+ })),
31324
+ ["id", "name", "products"]
31325
+ );
31326
+ if (result.droppedProductIds.length > 0) {
31327
+ process.stdout.write(
31328
+ `\u26A0\uFE0F Not stored (unknown, deleted or a bundle): ${result.droppedProductIds.join(", ")}
31329
+ `
31330
+ );
31331
+ }
31332
+ }
31333
+ function registerSiteLibrarySectionsCommands(site) {
31334
+ const sections = site.command("library-sections").description(
31335
+ "Sections of the member library (\u201EKrok 1\u201D, \u201EBonusy\u201D): a heading + a filter pill each; products in no section show under \u201EPozosta\u0142e\u201D. Products only, never bundles"
31336
+ );
31337
+ sections.command("list").description("The sections in display order with their products, and the unassigned products").action(async (_options, command) => {
31338
+ const flags = getGlobalFlags(command);
31339
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
31340
+ const result = await client.listLibrarySections();
31341
+ emit(flags, result, () => printSectionsView(result));
31342
+ });
31343
+ sections.command("create").description("Add a section (last by default); listed products move into it from any other").requiredOption("--name <name>", "heading + pill label, unique in the workspace").option("--products <a,b,...>", "product ids to put in it", parseCommaList("--products")).option(
31344
+ "--position <n>",
31345
+ "0-based place among the sections (default: last)",
31346
+ parseNumberOption("--position", NumberFormat.Integer)
31347
+ ).option("--confirm", "required \u2014 acknowledge the change of the member library").action(
31348
+ async (options, command) => {
31349
+ const flags = getGlobalFlags(command);
31350
+ requireConfirm(options.confirm, "add a section to the member library");
31351
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
31352
+ const result = await client.createLibrarySection({
31353
+ name: options.name,
31354
+ ...options.products !== void 0 ? { productIds: options.products } : {},
31355
+ ...options.position !== void 0 ? { position: options.position } : {}
31356
+ });
31357
+ emit(
31358
+ flags,
31359
+ result,
31360
+ () => printStoredSections(result, `Created section ${result.sectionId ?? ""}.`)
29061
31361
  );
29062
31362
  }
31363
+ );
31364
+ sections.command("rename").description("Rename a section (its id and products stay)").argument("<sectionId>", "section id (`libsec_\u2026` from `site library-sections list`)").requiredOption("--name <name>", "the new heading").option("--confirm", "required \u2014 acknowledge the change of the member library").action(
31365
+ async (sectionId, options, command) => {
31366
+ const flags = getGlobalFlags(command);
31367
+ requireConfirm(options.confirm, `rename section ${sectionId}`);
31368
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
31369
+ const result = await client.renameLibrarySection(sectionId, options.name);
31370
+ emit(flags, result, () => printStoredSections(result, `Renamed section ${sectionId}.`));
31371
+ }
31372
+ );
31373
+ sections.command("reorder").description("Set the section order \u2014 pass EVERY section id once, top first").argument("<sectionIds...>", "section ids in the new order").option("--confirm", "required \u2014 acknowledge the change of the member library").action(async (sectionIds, options, command) => {
31374
+ const flags = getGlobalFlags(command);
31375
+ requireConfirm(options.confirm, "reorder the library sections");
29063
31376
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
29064
- const result = await client.updateAudienceSegment(id, input);
29065
- emit(flags, result, () => {
29066
- const { segment } = result;
29067
- process.stdout.write(
29068
- `Updated segment ${segment.id} (${segment.name})${segment.lastEvaluatedCount !== null ? ` \u2014 ~${segment.lastEvaluatedCount} clients match` : ""}.
29069
- `
29070
- );
29071
- });
31377
+ const result = await client.reorderLibrarySections(sectionIds);
31378
+ emit(flags, result, () => printStoredSections(result, "Reordered the sections."));
29072
31379
  });
29073
- segments.command("delete").description(
29074
- "Delete a segment \u2014 campaigns already sent to it are unaffected; drafts targeting it will fail to send until re-pointed"
29075
- ).argument("<id>", "segment id").action(async (id, _options, command) => {
31380
+ sections.command("move").description("Move products into a section (they leave any other), or to none (\u201EPozosta\u0142e\u201D)").argument("<productIds...>", "product ids").requiredOption("--to <sectionId|none>", `target section id, or "${NO_SECTION}"`).option("--confirm", "required \u2014 acknowledge the change of the member library").action(
31381
+ async (productIds, options, command) => {
31382
+ const flags = getGlobalFlags(command);
31383
+ const sectionId = options.to.trim().toLowerCase() === NO_SECTION ? null : options.to;
31384
+ requireConfirm(options.confirm, "move products between library sections");
31385
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
31386
+ const result = await client.moveLibraryProducts(productIds, sectionId);
31387
+ emit(
31388
+ flags,
31389
+ result,
31390
+ () => printStoredSections(
31391
+ result,
31392
+ `Moved ${productIds.length} product(s) to ${sectionId ?? "no section"}.`
31393
+ )
31394
+ );
31395
+ }
31396
+ );
31397
+ sections.command("replace").description(
31398
+ "Full replace of the ordered list \u2014 sections left out are DELETED (their products go to \u201EPozosta\u0142e\u201D); keep a section by sending its id"
31399
+ ).requiredOption(
31400
+ "--sections <json|file>",
31401
+ "JSON array of {id?, name, productIds} (read the current list with `--json site library-sections list`)",
31402
+ parseJsonArrayFileOrInline("--sections")
31403
+ ).option("--confirm", "required \u2014 acknowledge the full replace").action(async (options, command) => {
29076
31404
  const flags = getGlobalFlags(command);
31405
+ const drafts = toSectionDrafts(options.sections);
31406
+ requireConfirm(options.confirm, "replace every library section");
29077
31407
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
29078
- const result = await client.deleteAudienceSegment(id);
29079
- emit(flags, result, () => {
29080
- process.stdout.write(`Deleted segment ${result.segmentId}.
29081
- `);
29082
- });
31408
+ const result = await client.replaceLibrarySections(drafts);
31409
+ emit(
31410
+ flags,
31411
+ result,
31412
+ () => printStoredSections(result, `Stored ${result.sections.length} section(s).`)
31413
+ );
29083
31414
  });
29084
- withConditionOptions(
29085
- segments.command("preview").description(
29086
- "Count the clients matching a condition set WITHOUT saving anything (stateless dry run)"
29087
- )
29088
- ).action(async (options, command) => {
31415
+ sections.command("delete").description("Delete a section; its products go back to \u201EPozosta\u0142e\u201D (no product is deleted)").argument("<sectionId>", "section id").option("--confirm", "required \u2014 acknowledge the deletion of the section").action(async (sectionId, options, command) => {
29089
31416
  const flags = getGlobalFlags(command);
29090
- const conditions = resolveConditions(options);
29091
- if (conditions === void 0) {
29092
- throw new Error(`Pass at least one condition: ${CONDITION_FLAGS_HELP}.`);
29093
- }
31417
+ requireConfirm(options.confirm, `delete library section ${sectionId}`);
29094
31418
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
29095
- const result = await client.previewAudienceSegment(conditions);
29096
- emit(flags, result, () => {
29097
- process.stdout.write(
29098
- `${result.count} client${result.count === 1 ? " matches" : "s match"} ${conditions.map(describeCondition).join(" AND ")}.
29099
- `
29100
- );
29101
- });
31419
+ const result = await client.deleteLibrarySection(sectionId);
31420
+ emit(flags, result, () => printStoredSections(result, `Deleted section ${sectionId}.`));
29102
31421
  });
29103
31422
  }
29104
31423
 
29105
31424
  // src/commands/site/index.ts
29106
- import { existsSync as existsSync4, readFileSync as readFileSync25 } from "fs";
29107
31425
  var MAIN_PAGE_MODES = {
29108
31426
  sales: "/",
29109
31427
  dashboard: "/dashboard",
@@ -29134,7 +31452,7 @@ function requireConfirm14(confirm, what) {
29134
31452
  }
29135
31453
  function parseJsonObjectFileOrInline5(value, flag) {
29136
31454
  const trimmed = value.trim();
29137
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync25(value, "utf8");
31455
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync26(value, "utf8");
29138
31456
  let parsed;
29139
31457
  try {
29140
31458
  parsed = JSON.parse(raw);
@@ -29149,7 +31467,7 @@ function parseJsonObjectFileOrInline5(value, flag) {
29149
31467
  function baseBody(json) {
29150
31468
  return json === void 0 ? {} : parseJsonObjectFileOrInline5(json, "--json");
29151
31469
  }
29152
- function parseOnOff4(value, flag) {
31470
+ function parseOnOff5(value, flag) {
29153
31471
  const v = value.trim().toLowerCase();
29154
31472
  if (v === "on" || v === "true") return true;
29155
31473
  if (v === "off" || v === "false") return false;
@@ -29186,7 +31504,7 @@ function parseDomainList(value, what) {
29186
31504
  return items;
29187
31505
  }
29188
31506
  function readMarkdownOrFile(value) {
29189
- return existsSync4(value) ? readFileSync25(value, "utf8") : value;
31507
+ return existsSync4(value) ? readFileSync26(value, "utf8") : value;
29190
31508
  }
29191
31509
  function assertNonEmptyBody(body, hint) {
29192
31510
  if (Object.keys(body).length === 0) {
@@ -29276,16 +31594,16 @@ function printDomainResults(results) {
29276
31594
  }
29277
31595
  }
29278
31596
  function printLegalDocuments(docs) {
29279
- const preview = (content) => content === null || content === "" ? "" : `${content.length} chars${content.length > 60 ? ` \u2014 "${content.slice(0, 60).replace(/\s+/g, " ")}\u2026"` : ` \u2014 "${content}"`}`;
31597
+ const preview2 = (content) => content === null || content === "" ? "" : `${content.length} chars${content.length > 60 ? ` \u2014 "${content.slice(0, 60).replace(/\s+/g, " ")}\u2026"` : ` \u2014 "${content}"`}`;
29280
31598
  printTable(
29281
31599
  [
29282
31600
  { field: "language", value: docs.language },
29283
31601
  { field: "termsUrl", value: docs.termsUrl },
29284
31602
  { field: "termsSlug", value: docs.termsSlug ?? "" },
29285
- { field: "termsContent", value: preview(docs.termsContent) },
31603
+ { field: "termsContent", value: preview2(docs.termsContent) },
29286
31604
  { field: "privacyPolicyUrl", value: docs.privacyPolicyUrl },
29287
31605
  { field: "privacyPolicySlug", value: docs.privacyPolicySlug ?? "" },
29288
- { field: "privacyPolicyContent", value: preview(docs.privacyPolicyContent) }
31606
+ { field: "privacyPolicyContent", value: preview2(docs.privacyPolicyContent) }
29289
31607
  ],
29290
31608
  ["field", "value"]
29291
31609
  );
@@ -29375,6 +31693,26 @@ function registerSiteCommands2(program2) {
29375
31693
  const result = await client.getSiteSettings();
29376
31694
  emit(flags, result, () => printSiteSummary(result.settings, result.communication));
29377
31695
  });
31696
+ site.command("scripts").description(
31697
+ "The site's own scripts (head / end of body of every page-editor page), read only: change them in the dashboard (Site \u2192 Settings \u2192 Custom scripts)"
31698
+ ).action(async (_options, command) => {
31699
+ const flags = getGlobalFlags(command);
31700
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
31701
+ const result = await client.getSiteCustomScripts();
31702
+ emit(flags, result, () => {
31703
+ if (!result.available) {
31704
+ process.stdout.write("Custom scripts are not available for this workspace.\n");
31705
+ return;
31706
+ }
31707
+ process.stdout.write(`head:
31708
+ ${result.head || "(empty)"}
31709
+
31710
+ `);
31711
+ process.stdout.write(`body end:
31712
+ ${result.bodyEnd || "(empty)"}
31713
+ `);
31714
+ });
31715
+ });
29378
31716
  const update = site.command("update").description(
29379
31717
  "Update ONE section of the site settings atomically. Every section accepts --json=<file|inline> (write it with `=` \u2014 a bare --json is the global JSON-output flag); typed flags override keys from --json"
29380
31718
  );
@@ -29386,9 +31724,12 @@ function registerSiteCommands2(program2) {
29386
31724
  ).option(
29387
31725
  "--robots-rules <text>",
29388
31726
  String.raw`extra robots.txt lines (User-agent / Allow / Disallow / Crawl-delay); separate lines with \n, or use --json`
31727
+ ).option(
31728
+ "--llms-txt <text|file|none>",
31729
+ "the site's own /llms.txt (Markdown, inline or a file path); none = back to the generated file"
29389
31730
  ).option("--language <pl|en|default>", "UI language of the public site").option("--show-page-title <on|off>", "render the page title in the header").option(
29390
31731
  "--json <fileOrJson>",
29391
- "inline JSON object or .json path \u2014 pageTitle, pageDescription, seoDescription, robotsTxtRules, showPageTitle, language, mainPage, mainPageId, mainPageCommunityId"
31732
+ "inline JSON object or .json path \u2014 pageTitle, pageDescription, seoDescription, robotsTxtRules, llmsTxt, showPageTitle, language, mainPage, mainPageId, mainPageCommunityId"
29392
31733
  ).action(
29393
31734
  async (options, command) => {
29394
31735
  const flags = getGlobalFlags(command);
@@ -29399,13 +31740,19 @@ function registerSiteCommands2(program2) {
29399
31740
  if (options.robotsRules !== void 0) {
29400
31741
  body.robotsTxtRules = options.robotsRules.split(String.raw`\n`).join("\n");
29401
31742
  }
31743
+ if (options.llmsTxt !== void 0) {
31744
+ body.llmsTxt = options.llmsTxt.trim().toLowerCase() === "none" ? null : readMarkdownOrFile(options.llmsTxt);
31745
+ }
29402
31746
  if (options.language !== void 0) {
29403
31747
  body.language = parseChoice7(options.language, PLATFORM_LANGUAGES, "--language");
29404
31748
  }
29405
31749
  if (options.showPageTitle !== void 0) {
29406
- body.showPageTitle = parseOnOff4(options.showPageTitle, "--show-page-title");
31750
+ body.showPageTitle = parseOnOff5(options.showPageTitle, "--show-page-title");
29407
31751
  }
29408
- assertNonEmptyBody(body, "pass --title, --description, --language or --json=<patch>.");
31752
+ assertNonEmptyBody(
31753
+ body,
31754
+ "pass --title, --description, --llms-txt, --language or --json=<patch>."
31755
+ );
29409
31756
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
29410
31757
  const result = await client.updateSiteGeneral(body).catch(withMainPageHint);
29411
31758
  emit(flags, result, () => process.stdout.write(SECTION_UPDATED("general")));
@@ -29425,7 +31772,7 @@ function registerSiteCommands2(program2) {
29425
31772
  if (options.radius !== void 0) body.radius = options.radius;
29426
31773
  if (options.styleVariant !== void 0) body.styleVariant = options.styleVariant;
29427
31774
  if (options.fromBrand !== void 0) {
29428
- body.themeFromBrand = parseOnOff4(options.fromBrand, "--from-brand");
31775
+ body.themeFromBrand = parseOnOff5(options.fromBrand, "--from-brand");
29429
31776
  }
29430
31777
  assertNonEmptyBody(body, "pass --theme, --radius, --from-brand \u2026 or --json=<patch>.");
29431
31778
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -29452,7 +31799,7 @@ function registerSiteCommands2(program2) {
29452
31799
  body.platformFaviconUrl = orNull6(options.faviconUrl);
29453
31800
  }
29454
31801
  if (options.hideBranding !== void 0) {
29455
- body.hideZanfiaBranding = parseOnOff4(options.hideBranding, "--hide-branding");
31802
+ body.hideZanfiaBranding = parseOnOff5(options.hideBranding, "--hide-branding");
29456
31803
  }
29457
31804
  assertNonEmptyBody(
29458
31805
  body,
@@ -29497,7 +31844,7 @@ function registerSiteCommands2(program2) {
29497
31844
  update.command("privacy").description("Cookie-consent banner on the public site").option("--cookie-consent <on|off>", "show the cookie consent banner").option("--json <fileOrJson>", "inline JSON object or .json path \u2014 cookieConsentEnabled").action(async (options, command) => {
29498
31845
  const flags = getGlobalFlags(command);
29499
31846
  const base = baseBody(options.json);
29500
- const cookieConsentEnabled = options.cookieConsent !== void 0 ? parseOnOff4(options.cookieConsent, "--cookie-consent") : base.cookieConsentEnabled;
31847
+ const cookieConsentEnabled = options.cookieConsent !== void 0 ? parseOnOff5(options.cookieConsent, "--cookie-consent") : base.cookieConsentEnabled;
29501
31848
  if (typeof cookieConsentEnabled !== "boolean") {
29502
31849
  throw new Error(
29503
31850
  'Pass --cookie-consent on|off (or a --json body with "cookieConsentEnabled").'
@@ -29564,14 +31911,14 @@ function registerSiteCommands2(program2) {
29564
31911
  if (options.libraryOrder === void 0) customization.libraryOrder = "custom";
29565
31912
  }
29566
31913
  if (options.groupByType !== void 0) {
29567
- customization.libraryGroupByType = parseOnOff4(options.groupByType, "--group-by-type");
31914
+ customization.libraryGroupByType = parseOnOff5(options.groupByType, "--group-by-type");
29568
31915
  }
29569
31916
  if (options.defaultView !== void 0) customization.defaultView = options.defaultView;
29570
31917
  if (options.messages !== void 0) {
29571
- customization.messagesEnabled = parseOnOff4(options.messages, "--messages");
31918
+ customization.messagesEnabled = parseOnOff5(options.messages, "--messages");
29572
31919
  }
29573
31920
  if (options.clientMessages !== void 0) {
29574
- customization.clientMessagesEnabled = parseOnOff4(
31921
+ customization.clientMessagesEnabled = parseOnOff5(
29575
31922
  options.clientMessages,
29576
31923
  "--client-messages"
29577
31924
  );
@@ -29810,6 +32157,7 @@ function registerSiteCommands2(program2) {
29810
32157
  });
29811
32158
  }
29812
32159
  );
32160
+ registerSiteLibrarySectionsCommands(site);
29813
32161
  }
29814
32162
  function printStorefront(config) {
29815
32163
  printTable(
@@ -29847,13 +32195,15 @@ function printStorefront(config) {
29847
32195
  }
29848
32196
 
29849
32197
  // src/commands/smsCampaigns/index.ts
29850
- import { readFileSync as readFileSync26 } from "fs";
32198
+ import { readFileSync as readFileSync27 } from "fs";
29851
32199
  var SMS_PROVIDERS2 = ["twilio", "smsapi"];
29852
32200
  var RECIPIENT_STATUSES2 = [
29853
32201
  "pending",
29854
32202
  "sent",
29855
32203
  "failed",
29856
- "skipped-invalid-number"
32204
+ "skipped-invalid-number",
32205
+ "skipped-duplicate-number",
32206
+ "skipped-opted-out-number"
29857
32207
  ];
29858
32208
  var SCHEDULER_UNAVAILABLE_STATUS4 = 503;
29859
32209
  function parsePositiveInt15(value, flag) {
@@ -29880,7 +32230,7 @@ function resolveBody(options) {
29880
32230
  if (options.body !== void 0 && options.bodyFile !== void 0) {
29881
32231
  throw new Error("Use either --body or --body-file, not both.");
29882
32232
  }
29883
- return options.bodyFile !== void 0 ? readFileSync26(options.bodyFile, "utf8") : options.body;
32233
+ return options.bodyFile !== void 0 ? readFileSync27(options.bodyFile, "utf8") : options.body;
29884
32234
  }
29885
32235
  function resolveAudience2(options) {
29886
32236
  if (options.segment !== void 0 && options.allSubscribed) {
@@ -29946,7 +32296,7 @@ function printCampaignDetail2(campaign) {
29946
32296
  `);
29947
32297
  const { stats } = campaign;
29948
32298
  process.stdout.write(
29949
- ` Stats: sent ${stats.sent}, failed ${stats.failed}, skipped (invalid number) ${stats.skippedInvalidNumber}, billed parts ${stats.segments}
32299
+ ` Stats: sent ${stats.sent}, failed ${stats.failed}, skipped (invalid number) ${stats.skippedInvalidNumber}, skipped (duplicate number) ${stats.skippedDuplicateNumber ?? 0}, skipped (opted-out number) ${stats.skippedOptedOutNumber ?? 0}, billed parts ${stats.segments}
29950
32300
  `
29951
32301
  );
29952
32302
  process.stdout.write(` Opt-out line: ${campaign.optOutHint ?? "(none)"}
@@ -30321,6 +32671,96 @@ Next page: --after ${response.nextCursor}`);
30321
32671
  });
30322
32672
  }
30323
32673
 
32674
+ // src/commands/social/insights.ts
32675
+ var HIDDEN = { hidden: true };
32676
+ function parsePrivacy(value) {
32677
+ const privacy = YOUTUBE_PRIVACY_STATUSES.find(
32678
+ (candidate) => candidate === value.trim().toLowerCase()
32679
+ );
32680
+ if (privacy === void 0) {
32681
+ throw new Error(
32682
+ `Invalid --privacy: ${value} (expected one of: ${YOUTUBE_PRIVACY_STATUSES.join(", ")})`
32683
+ );
32684
+ }
32685
+ return privacy;
32686
+ }
32687
+ var formatNumber = (value) => value ?? "";
32688
+ function registerSocialInsightsCommands(social) {
32689
+ social.command("weekly", HIDDEN).description(
32690
+ "Studio \u201ETw\xF3j tydzie\u0144 w social media\u201D: last week's posts per account against your own median (top / bottom posts and signals in `--json`)"
32691
+ ).action(async (_options, command) => {
32692
+ const flags = getGlobalFlags(command);
32693
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
32694
+ const report = await client.getSocialWeekReport();
32695
+ emit(flags, report, () => {
32696
+ process.stdout.write(`Week ${report.weekStart} \u2192 ${report.weekEnd}
32697
+ `);
32698
+ printTable(
32699
+ report.networks.map((network) => ({
32700
+ network: network.network,
32701
+ account: network.label,
32702
+ state: network.state,
32703
+ metric: network.metric ?? "",
32704
+ median: formatNumber(network.baselineMedian),
32705
+ postsThisWeek: network.postsThisWeek
32706
+ })),
32707
+ ["network", "account", "state", "metric", "median", "postsThisWeek"]
32708
+ );
32709
+ });
32710
+ });
32711
+ social.command("performance", HIDDEN).description(
32712
+ "One Zanfia post's numbers per target against your median (\u201Eten post jeszcze \u017Cyje\u201D); the curve in `--json`"
32713
+ ).argument("<postId>", "social post id (from `social posts`)").action(async (postId, _options, command) => {
32714
+ const flags = getGlobalFlags(command);
32715
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
32716
+ const performance = await client.getSocialPostPerformance(postId);
32717
+ emit(
32718
+ flags,
32719
+ performance,
32720
+ () => printTable(
32721
+ performance.targets.map((target) => ({
32722
+ target: target.target,
32723
+ state: target.state,
32724
+ metric: target.metric ?? "",
32725
+ score: formatNumber(target.score),
32726
+ median: formatNumber(target.baselineMedian),
32727
+ publishedAt: target.publishedAt ?? ""
32728
+ })),
32729
+ ["target", "state", "metric", "score", "median", "publishedAt"]
32730
+ )
32731
+ );
32732
+ });
32733
+ const youtube = social.commands.find((command) => command.name() === "youtube") ?? social.command("youtube");
32734
+ youtube.command("visibility", HIDDEN).description(
32735
+ "Change the visibility of a post's published YouTube video (private / unlisted / public), or schedule its public release. Requires --confirm"
32736
+ ).argument("<postId>", "the social post that published the video").requiredOption("--privacy <private|unlisted|public>", "the new visibility", parsePrivacy).option(
32737
+ "--publish-at <iso>",
32738
+ "public release time, ISO 8601 with an offset (only with --privacy private)"
32739
+ ).option("--confirm", "required \u2014 acknowledge the change on YouTube").action(
32740
+ async (postId, options, command) => {
32741
+ const flags = getGlobalFlags(command);
32742
+ if (options.publishAt !== void 0 && options.privacy !== "private") {
32743
+ throw new Error("--publish-at needs --privacy private (YouTube makes it public then).");
32744
+ }
32745
+ requireConfirm(options.confirm, `change the YouTube visibility of post ${postId}`);
32746
+ const input = {
32747
+ privacyStatus: options.privacy,
32748
+ ...options.publishAt !== void 0 ? { publishAt: options.publishAt } : {}
32749
+ };
32750
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
32751
+ const result = await client.setYouTubeVideoVisibility(postId, input);
32752
+ emit(
32753
+ flags,
32754
+ result,
32755
+ () => process.stdout.write(
32756
+ `Video ${result.videoId} is ${result.privacyStatus}${result.publishAt ? `, public from ${result.publishAt}` : ""}${result.demo ? " (demo: nothing was sent to YouTube)" : ""}.
32757
+ `
32758
+ )
32759
+ );
32760
+ }
32761
+ );
32762
+ }
32763
+
30324
32764
  // src/commands/social/index.ts
30325
32765
  function updateInput(options) {
30326
32766
  const targets = splitList(options.targets);
@@ -30365,11 +32805,13 @@ function composeInput(options) {
30365
32805
  ...options.publish ? { publish: true } : {}
30366
32806
  };
30367
32807
  }
30368
- function registerSocialCommands(program2) {
32808
+ function registerSocialCommands2(program2) {
30369
32809
  const social = program2.command("social").description(
30370
32810
  "Social media \u2014 connected accounts, posts (drafts, scheduling, publishing) and the stats of the connected accounts (youtube / instagram / threads / snapshot)"
30371
32811
  );
30372
- social.command("accounts").description("List connected social accounts (the values to pass as --targets)").action(async (_, command) => {
32812
+ social.command("accounts").description(
32813
+ "List connected social accounts (the values to pass as --targets; a bare network name such as `instagram` goes to the account marked default)"
32814
+ ).action(async (_, command) => {
30373
32815
  const flags = getGlobalFlags(command);
30374
32816
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
30375
32817
  const response = await client.listSocialAccounts();
@@ -30379,11 +32821,12 @@ function registerSocialCommands(program2) {
30379
32821
  target: a.target,
30380
32822
  network: a.network,
30381
32823
  label: a.label,
32824
+ default: a.isDefault ? "yes" : "no",
30382
32825
  schedule: a.supportsScheduling ? "yes" : "no",
30383
32826
  publishNow: a.supportsImmediatePublish ? "yes" : "no",
30384
32827
  firstComment: a.firstCommentNeedsReconnect ? "reconnect" : ""
30385
32828
  })),
30386
- ["target", "network", "label", "schedule", "publishNow", "firstComment"]
32829
+ ["target", "network", "label", "default", "schedule", "publishNow", "firstComment"]
30387
32830
  );
30388
32831
  });
30389
32832
  });
@@ -30516,6 +32959,7 @@ function registerSocialCommands(program2) {
30516
32959
  });
30517
32960
  });
30518
32961
  registerSocialChannelCommands(social);
32962
+ registerSocialInsightsCommands(social);
30519
32963
  }
30520
32964
 
30521
32965
  // src/commands/stats/index.ts
@@ -30928,6 +33372,148 @@ ${integration.name}: ${integration.healthExplanation}
30928
33372
  });
30929
33373
  }
30930
33374
 
33375
+ // src/commands/studio/drafts.ts
33376
+ import { readFileSync as readFileSync28 } from "fs";
33377
+ var TEXT_PREVIEW_LENGTH = 60;
33378
+ function parseWeakReason(value) {
33379
+ const reason = STUDIO_SOCIAL_DRAFT_WEAK_REASONS.find(
33380
+ (candidate) => candidate === value.trim().toLowerCase()
33381
+ );
33382
+ if (reason === void 0) {
33383
+ throw new Error(
33384
+ `Invalid --reason: ${value} (expected one of: ${STUDIO_SOCIAL_DRAFT_WEAK_REASONS.join(", ")})`
33385
+ );
33386
+ }
33387
+ return reason;
33388
+ }
33389
+ var SINGLE_ACCOUNT_TARGETS = [
33390
+ "instagram",
33391
+ "linkedin",
33392
+ "threads",
33393
+ "youtube",
33394
+ "tiktok",
33395
+ "telegram"
33396
+ ];
33397
+ function parseTarget(value) {
33398
+ const target = value.trim();
33399
+ const single = SINGLE_ACCOUNT_TARGETS.find((candidate) => candidate === target);
33400
+ if (single !== void 0) return single;
33401
+ const separator = target.indexOf(":");
33402
+ const network = target.slice(0, separator);
33403
+ const accountId = target.slice(separator + 1);
33404
+ if (separator > 0 && accountId !== "") {
33405
+ if (network === "facebook") return `facebook:${accountId}`;
33406
+ if (network === "instagram") return `instagram:${accountId}`;
33407
+ }
33408
+ throw new Error(
33409
+ `Invalid target: ${value} (expected ${SINGLE_ACCOUNT_TARGETS.join(", ")}, facebook:<pageId> or instagram:<accountId>; see \`social accounts\`)`
33410
+ );
33411
+ }
33412
+ function preview(text) {
33413
+ const oneLine2 = text.replace(/\s+/g, " ").trim();
33414
+ return oneLine2.length > TEXT_PREVIEW_LENGTH ? `${oneLine2.slice(0, TEXT_PREVIEW_LENGTH)}\u2026` : oneLine2;
33415
+ }
33416
+ function printDrafts(accounts) {
33417
+ const rows = accounts.flatMap(
33418
+ (account) => account.drafts.map((draft) => ({
33419
+ id: draft.id,
33420
+ account: account.label,
33421
+ slot: draft.slot,
33422
+ status: draft.status,
33423
+ text: preview(draft.text)
33424
+ }))
33425
+ );
33426
+ printTable(rows, ["id", "account", "slot", "status", "text"]);
33427
+ for (const account of accounts) {
33428
+ process.stdout.write(`${account.label} (${account.target}): ${account.remaining} left
33429
+ `);
33430
+ }
33431
+ }
33432
+ function registerStudioDraftsCommands(program2) {
33433
+ const studio = program2.commands.find((command) => command.name() === "studio") ?? program2.command("studio");
33434
+ const drafts = studio.command("drafts", { hidden: true }).description("This week's social post drafts in your voice (Studio \u201E5 szkic\xF3w\u201D)");
33435
+ drafts.command("list").description("This week's drafts per connected account and how many each may still get").action(async (_options, command) => {
33436
+ const flags = getGlobalFlags(command);
33437
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
33438
+ const result = await client.listStudioSocialDrafts();
33439
+ emit(flags, result, () => {
33440
+ process.stdout.write(`Week of ${result.week}
33441
+ `);
33442
+ printDrafts(result.accounts);
33443
+ });
33444
+ });
33445
+ drafts.command("generate").description(
33446
+ "Write this week's drafts for one account; spends AI credits (402 when they are out)"
33447
+ ).requiredOption("--target <target>", "the account target (from `social accounts`)", parseTarget).option("--topic <text>", "what the posts should be about").option("--language <pl|en>", "default: the workspace language").option("--confirm", "required \u2014 acknowledge the AI credits spent").action(
33448
+ async (options, command) => {
33449
+ const flags = getGlobalFlags(command);
33450
+ requireConfirm(options.confirm, "spend AI credits on new drafts");
33451
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
33452
+ const result = await client.generateStudioSocialDrafts({
33453
+ target: options.target,
33454
+ ...options.topic !== void 0 ? { topic: options.topic } : {},
33455
+ ...options.language !== void 0 ? { language: options.language } : {}
33456
+ });
33457
+ emit(flags, result, () => {
33458
+ process.stdout.write(
33459
+ `Wrote ${result.created} draft(s) for ${result.account.label}${result.rejectedByAiTells > 0 ? ` (${result.rejectedByAiTells} held back)` : ""}.
33460
+ `
33461
+ );
33462
+ printDrafts([result.account]);
33463
+ });
33464
+ }
33465
+ );
33466
+ drafts.command("feedback").description(
33467
+ "Mark a draft \u201ES\u0142abe\u201D with a reason: it is hidden and the next drafts learn from it"
33468
+ ).argument("<draftId>", "draft id (from `studio drafts list`)").requiredOption(
33469
+ `--reason <${STUDIO_SOCIAL_DRAFT_WEAK_REASONS.join("|")}>`,
33470
+ "why it is weak",
33471
+ parseWeakReason
33472
+ ).option("--confirm", "required \u2014 acknowledge the draft is hidden").action(
33473
+ async (draftId, options, command) => {
33474
+ const flags = getGlobalFlags(command);
33475
+ requireConfirm(options.confirm, `mark draft ${draftId} as weak`);
33476
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
33477
+ const result = await client.sendStudioSocialDraftFeedback(draftId, options.reason);
33478
+ emit(
33479
+ flags,
33480
+ result,
33481
+ () => process.stdout.write(
33482
+ `Draft ${draftId} is marked weak.${result.learnedRule ? ` Learned: ${result.learnedRule}` : ""}
33483
+ `
33484
+ )
33485
+ );
33486
+ }
33487
+ );
33488
+ drafts.command("to-composer").description("Turn a draft into a DRAFT social post (nothing is published or scheduled)").argument("<draftId>", "draft id").option("--text <text>", "the edited text (default: the draft as written)").option("--text-file <path>", "read the edited text from a file").option(
33489
+ "--targets <a,b,...>",
33490
+ "accounts to post to (default: the draft's own account)",
33491
+ parseCommaList("--targets")
33492
+ ).option("--confirm", "required \u2014 acknowledge a draft post is created").action(
33493
+ async (draftId, options, command) => {
33494
+ const flags = getGlobalFlags(command);
33495
+ if (options.text !== void 0 && options.textFile !== void 0) {
33496
+ throw new Error("Use either --text or --text-file, not both.");
33497
+ }
33498
+ const text = options.textFile !== void 0 ? readFileSync28(options.textFile, "utf8") : options.text;
33499
+ requireConfirm(options.confirm, `create a draft social post from ${draftId}`);
33500
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
33501
+ const result = await client.sendStudioSocialDraftToComposer(draftId, {
33502
+ ...text !== void 0 ? { text } : {},
33503
+ ...options.targets !== void 0 ? { targets: options.targets.map((target) => parseTarget(target)) } : {}
33504
+ });
33505
+ emit(
33506
+ flags,
33507
+ result,
33508
+ () => process.stdout.write(
33509
+ `Draft post ${result.socialPostId} is in Social \u203A Posty (nothing is published).
33510
+ `
33511
+ )
33512
+ );
33513
+ }
33514
+ );
33515
+ }
33516
+
30931
33517
  // src/commands/subscriptions/index.ts
30932
33518
  function formatAmount4(amountMinor, currency) {
30933
33519
  return `${(amountMinor / 100).toFixed(2)} ${currency.toUpperCase()}`;
@@ -30965,7 +33551,7 @@ function describeRetryStatus(status) {
30965
33551
  return "Failed \u2014 the retry did not go through.";
30966
33552
  }
30967
33553
  }
30968
- function registerSubscriptionsCommands(program2) {
33554
+ function registerSubscriptionsCommands2(program2) {
30969
33555
  const subscriptions = program2.command("subscriptions").description("Recurring billing: failed payments, manual renewals, subscriptions ending soon");
30970
33556
  subscriptions.command("failed").description(
30971
33557
  "Failed subscription / installment charges with live Stripe dunning state (attempts, next automatic retry, hosted invoice URL). Retry one with `subscriptions retry`"
@@ -31557,7 +34143,7 @@ Next page: --cursor ${page.nextCursor}
31557
34143
  }
31558
34144
 
31559
34145
  // src/commands/workflows/index.ts
31560
- import { readFileSync as readFileSync27 } from "fs";
34146
+ import { readFileSync as readFileSync30 } from "fs";
31561
34147
 
31562
34148
  // src/commands/workflows/authoring.ts
31563
34149
  import { writeFileSync as writeFileSync6 } from "fs";
@@ -31725,6 +34311,89 @@ function registerWorkflowAuthoringCommands(workflows) {
31725
34311
  });
31726
34312
  }
31727
34313
 
34314
+ // src/commands/workflows/emailConditions.ts
34315
+ import { readFileSync as readFileSync29 } from "fs";
34316
+ function collectRepeatable8(value, previous) {
34317
+ return [...previous, value];
34318
+ }
34319
+ function parseCustomValues(pairs) {
34320
+ const values = {};
34321
+ for (const pair of pairs) {
34322
+ const at = pair.indexOf("=");
34323
+ const key = at > 0 ? pair.slice(0, at).trim().replace(/^custom\./, "") : "";
34324
+ if (!key) throw new Error(`Invalid --custom ${pair} (expected <key>=<value>)`);
34325
+ values[key] = pair.slice(at + 1);
34326
+ }
34327
+ return values;
34328
+ }
34329
+ function buildInput(options) {
34330
+ if (options.body !== void 0 && options.bodyFile !== void 0) {
34331
+ throw new Error("Pass either --body or --body-file, not both.");
34332
+ }
34333
+ const body = options.bodyFile !== void 0 ? readFileSync29(options.bodyFile, "utf8") : options.body;
34334
+ if (options.subject === void 0 && options.preheader === void 0 && body === void 0) {
34335
+ throw new Error("Pass at least one of --subject, --preheader, --body or --body-file.");
34336
+ }
34337
+ const custom = parseCustomValues(options.custom);
34338
+ const withContact = options.firstName !== void 0 || Object.keys(custom).length > 0;
34339
+ return {
34340
+ ...options.subject !== void 0 ? { subject: options.subject } : {},
34341
+ ...options.preheader !== void 0 ? { preheader: options.preheader } : {},
34342
+ ...body !== void 0 ? { body } : {},
34343
+ ...withContact ? {
34344
+ exampleContact: {
34345
+ ...options.firstName !== void 0 ? { firstName: options.firstName } : {},
34346
+ ...Object.keys(custom).length > 0 ? { custom } : {}
34347
+ }
34348
+ } : {}
34349
+ };
34350
+ }
34351
+ function printResult(result) {
34352
+ const lines = [];
34353
+ if (!result.hasConditions) {
34354
+ lines.push("No {% %} conditions in these texts.");
34355
+ } else if (result.valid) {
34356
+ lines.push("Every condition tag parses.");
34357
+ } else {
34358
+ lines.push(`${result.warnings.length} problem(s); the send would use the safe text:`);
34359
+ for (const warning of result.warnings) {
34360
+ lines.push(` [${warning.field}] ${warning.tag} (${warning.code}): ${warning.detail}`);
34361
+ }
34362
+ }
34363
+ if (result.customFieldKeys.length > 0) {
34364
+ lines.push(`Custom fields read: ${result.customFieldKeys.join(", ")}`);
34365
+ }
34366
+ if (result.exampleResult) {
34367
+ lines.push("For the example contact:");
34368
+ for (const field of ["subject", "preheader", "body"]) {
34369
+ const text = result.exampleResult[field];
34370
+ if (text !== void 0) lines.push(` ${field}: ${text}`);
34371
+ }
34372
+ }
34373
+ if (result.evaluatedForWorkspace) {
34374
+ lines.push("Conditions are evaluated in this workspace (workflow and library e-mails).");
34375
+ }
34376
+ if (result.note) lines.push(result.note);
34377
+ process.stdout.write(`${lines.join("\n")}
34378
+ `);
34379
+ }
34380
+ function registerEmailConditionsCommand(workflows) {
34381
+ workflows.command("check-email-conditions").description(
34382
+ "Check {% if %} / {% case %} conditional content in a workflow or library e-mail before saving it (read-only; not for e-mail campaigns, which send the default version)"
34383
+ ).option("--subject <text>", "e-mail subject, as it will be saved").option("--preheader <text>", "inbox preview line").option("--body <markdown>", "e-mail body (markdown)").option("--body-file <path>", "read the body (markdown) from a file").option("--first-name <name>", "example contact: first name").option(
34384
+ "--custom <key=value>",
34385
+ "example contact: a custom field value as the e-mail prints it (a select = its option label; repeatable)",
34386
+ collectRepeatable8,
34387
+ []
34388
+ ).action(async (options, command) => {
34389
+ const flags = getGlobalFlags(command);
34390
+ const input = buildInput(options);
34391
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
34392
+ const result = await client.checkEmailConditions(input);
34393
+ emit(flags, result, () => printResult(result));
34394
+ });
34395
+ }
34396
+
31728
34397
  // src/commands/workflows/index.ts
31729
34398
  function parsePositiveInt18(value, flag) {
31730
34399
  const n = Number.parseInt(value, 10);
@@ -31740,7 +34409,7 @@ function requireConfirm17(confirm, what) {
31740
34409
  }
31741
34410
  function parseTriggerContext(value) {
31742
34411
  const trimmed = value.trim();
31743
- const raw = trimmed.startsWith("{") ? trimmed : readFileSync27(value, "utf8");
34412
+ const raw = trimmed.startsWith("{") ? trimmed : readFileSync30(value, "utf8");
31744
34413
  let parsed;
31745
34414
  try {
31746
34415
  parsed = JSON.parse(raw);
@@ -31809,7 +34478,9 @@ function registerWorkflowsCommands(program2) {
31809
34478
  printWorkflowGraphs(workflow, published, draft, templateNames);
31810
34479
  });
31811
34480
  });
31812
- workflows.command("runs").description("List recent runs of a workflow (newest first)").argument("<automationId>", "workflow automation id").option("--limit <n>", "number of runs to return (default 20, max 100)").action(async (automationId, options, command) => {
34481
+ const runs = workflows.command("runs").description(
34482
+ "List recent runs of a workflow (newest first); `workflows runs stop` takes one person out of a run"
34483
+ ).argument("<automationId>", "workflow automation id").option("--limit <n>", "number of runs to return (default 20, max 100)").action(async (automationId, options, command) => {
31813
34484
  const flags = getGlobalFlags(command);
31814
34485
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
31815
34486
  const page = await client.listAutomationRuns(
@@ -31831,6 +34502,23 @@ function registerWorkflowsCommands(program2) {
31831
34502
  );
31832
34503
  });
31833
34504
  });
34505
+ runs.command("stop").description(
34506
+ "Take the person out of this run \u2014 it ends cancelled and nothing after the current step runs (a step already sending finishes). A finished run is left as it is. They can enter again when the trigger fires later. Requires --confirm"
34507
+ ).argument("<automationId>", "workflow automation id").argument("<runId>", "run id (from `workflows runs` or `clients automation-runs list`)").option("--confirm", "required \u2014 acknowledge that the run ends for this person").action(
34508
+ async (automationId, runId, options, command) => {
34509
+ const flags = getGlobalFlags(command);
34510
+ requireConfirm17(options.confirm, `stop run ${runId} (no further steps for this person)`);
34511
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
34512
+ const result = await client.stopAutomationRun(automationId, runId);
34513
+ emit(flags, result, () => {
34514
+ process.stdout.write(
34515
+ result.stopped.length > 0 ? `Stopped run ${runId} of workflow ${automationId}.
34516
+ ` : `Run ${runId} had already finished \u2014 nothing to stop.
34517
+ `
34518
+ );
34519
+ });
34520
+ }
34521
+ );
31834
34522
  workflows.command("pause").description(
31835
34523
  "Pause a workflow \u2014 it stops reacting to its trigger events AND cancels runs that are in progress or waiting in a delay (those contacts get no further steps)"
31836
34524
  ).argument("<automationId>", "workflow automation id").action(async (automationId, _, command) => {
@@ -31846,13 +34534,36 @@ function registerWorkflowsCommands(program2) {
31846
34534
  });
31847
34535
  workflows.command("resume").description(
31848
34536
  "Resume a paused workflow (requires a published version \u2014 a never-published draft cannot run)"
31849
- ).argument("<automationId>", "workflow automation id").action(async (automationId, _, command) => {
34537
+ ).argument("<automationId>", "workflow automation id").option(
34538
+ "--held-events <decision>",
34539
+ "a webhook paused after failed deliveries holds the events that reached it (14 days each): `send` delivers them (30 a minute; a failure pauses it again and the rest wait), `skip` drops them. Without it such a resume is refused with the count."
34540
+ ).action(async (automationId, options, command) => {
31850
34541
  const flags = getGlobalFlags(command);
34542
+ const heldEvents = HELD_EVENTS_DECISIONS.find((value) => value === options.heldEvents);
34543
+ if (options.heldEvents !== void 0 && !heldEvents) {
34544
+ throw new ApiError(0, "--held-events must be `send` or `skip`");
34545
+ }
31851
34546
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
31852
- const result = await client.setAutomationStatus(automationId, "active");
34547
+ const result = await client.setAutomationStatus(automationId, "active", heldEvents).catch((error) => {
34548
+ if (error instanceof ApiError && error.status === 409 && !heldEvents) {
34549
+ throw new ApiError(
34550
+ 409,
34551
+ error.apiMessage,
34552
+ "Run again with --held-events send (deliver them) or --held-events skip (drop them)."
34553
+ );
34554
+ }
34555
+ throw error;
34556
+ });
31853
34557
  emit(flags, result, () => {
31854
34558
  process.stdout.write(`Resumed workflow ${result.id}.
31855
34559
  `);
34560
+ if (result.heldEvents) {
34561
+ process.stdout.write(
34562
+ result.heldEvents.decision === "send" ? `${result.heldEvents.count} held event(s) are being sent, 30 a minute.
34563
+ ` : `${result.heldEvents.count} held event(s) were dropped.
34564
+ `
34565
+ );
34566
+ }
31856
34567
  });
31857
34568
  });
31858
34569
  workflows.command("retry").description(
@@ -31903,6 +34614,7 @@ Follow it with: zanfia workflows runs ${automationId}
31903
34614
  }
31904
34615
  );
31905
34616
  registerWorkflowAuthoringCommands(workflows);
34617
+ registerEmailConditionsCommand(workflows);
31906
34618
  }
31907
34619
  function formatProductScope(workflow) {
31908
34620
  return workflow.productIds.length > 0 ? String(workflow.productIds.length) : "all";
@@ -32269,7 +34981,7 @@ function describeDelay(config) {
32269
34981
  return `wait until ${optionalString(config.date) ?? "?"}${optionalString(config.timezone) ? ` (${config.timezone})` : ""}`;
32270
34982
  case "untilContextDate": {
32271
34983
  const offset = typeof config.offsetMinutes === "number" ? config.offsetMinutes : 0;
32272
- return `wait until ${optionalString(config.contextKey) ?? "?"}${offset !== 0 ? ` ${offset > 0 ? "+" : ""}${offset} min` : ""}${config.skipWhenPast === true ? " (skip branch when past)" : ""}`;
34984
+ return `wait until ${optionalString(config.contextKey) ?? "?"}${offset !== 0 ? ` ${offset > 0 ? "+" : ""}${offset} min` : ""}${config.skipWhenPast === true ? config.skipToNextWait === true ? " (skip to the next wait when past)" : " (skip branch when past)" : ""}`;
32273
34985
  }
32274
34986
  default:
32275
34987
  return `wait (${mode})`;
@@ -32324,7 +35036,7 @@ function printLogins(profiles) {
32324
35036
  "\n* = active. Switch: `zanfia workspace use <id|slug>`; one command: `--workspace <id|slug>`.\n"
32325
35037
  );
32326
35038
  }
32327
- function parseOnOff5(value, flag) {
35039
+ function parseOnOff6(value, flag) {
32328
35040
  const match = ["on", "off"].find((v) => v === value);
32329
35041
  if (!match) throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
32330
35042
  return match === "on";
@@ -32407,7 +35119,7 @@ function registerWorkspaceCommands(program2) {
32407
35119
  }
32408
35120
  if (options.inheritTimezone) input.timezone = null;
32409
35121
  if (options.autoTranscribe !== void 0) {
32410
- input.autoTranscribeUploads = parseOnOff5(options.autoTranscribe, "--auto-transcribe");
35122
+ input.autoTranscribeUploads = parseOnOff6(options.autoTranscribe, "--auto-transcribe");
32411
35123
  }
32412
35124
  if (options.transcriptionLanguage !== void 0) {
32413
35125
  input.transcriptionLanguage = parseTranscriptionLanguage(options.transcriptionLanguage);
@@ -32478,11 +35190,11 @@ function applyShortHelp(program2) {
32478
35190
 
32479
35191
  // src/program.ts
32480
35192
  function resolveCliVersion() {
32481
- if ("0.21.0".length > 0) {
32482
- return "0.21.0";
35193
+ if ("0.25.0".length > 0) {
35194
+ return "0.25.0";
32483
35195
  }
32484
35196
  try {
32485
- const raw = readFileSync28(new URL("../package.json", import.meta.url), "utf8");
35197
+ const raw = readFileSync31(new URL("../package.json", import.meta.url), "utf8");
32486
35198
  const parsed = JSON.parse(raw);
32487
35199
  if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
32488
35200
  const { version: version2 } = parsed;
@@ -32523,7 +35235,7 @@ Signing in \u2014 two roles, two consent pages:
32523
35235
  registerJoinCommand(program2);
32524
35236
  registerLogoutCommand(program2);
32525
35237
  registerAuthCommands(program2);
32526
- registerProductsCommands(program2);
35238
+ registerProductsCommands2(program2);
32527
35239
  registerOffersCommands(program2);
32528
35240
  registerCheckoutSettingsCommands(program2);
32529
35241
  registerIntegrationsCommands(program2);
@@ -32532,7 +35244,7 @@ Signing in \u2014 two roles, two consent pages:
32532
35244
  registerOtoCommands(program2);
32533
35245
  registerBumpsCommands(program2);
32534
35246
  registerCoursesCommands(program2);
32535
- registerGeneratedCommands(program2);
35247
+ registerConsultationsCommands(program2);
32536
35248
  registerNewslettersCommands(program2);
32537
35249
  registerCommunitiesCommands(program2);
32538
35250
  registerConversationsCommands(program2);
@@ -32545,8 +35257,8 @@ Signing in \u2014 two roles, two consent pages:
32545
35257
  registerTestimonialsCommands(program2);
32546
35258
  registerBrandCommands(program2);
32547
35259
  registerTeamCommands(program2);
32548
- registerOrdersCommands(program2);
32549
- registerSubscriptionsCommands(program2);
35260
+ registerOrdersCommands2(program2);
35261
+ registerSubscriptionsCommands2(program2);
32550
35262
  registerStatsCommands(program2);
32551
35263
  registerDiscountsCommands(program2);
32552
35264
  registerReferralsCommands(program2);
@@ -32555,9 +35267,11 @@ Signing in \u2014 two roles, two consent pages:
32555
35267
  registerWorkspaceCommands(program2);
32556
35268
  registerFunnelsCommands(program2);
32557
35269
  registerPagesCommands(program2);
32558
- registerSocialCommands(program2);
35270
+ registerSocialCommands2(program2);
32559
35271
  registerMediaCommands(program2);
32560
35272
  registerFilesCommands(program2);
35273
+ registerStudioDraftsCommands(program2);
35274
+ registerGeneratedCommands(program2);
32561
35275
  applyGlobalOptions(program2);
32562
35276
  applyShortHelp(program2);
32563
35277
  return program2;