@zanfia/cli 0.3.26 → 0.3.47
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.
- package/dist/index.js +1962 -306
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6900,6 +6900,9 @@ var {
|
|
|
6900
6900
|
Help
|
|
6901
6901
|
} = import_index.default;
|
|
6902
6902
|
|
|
6903
|
+
// src/program.ts
|
|
6904
|
+
import { readFileSync as readFileSync26 } from "fs";
|
|
6905
|
+
|
|
6903
6906
|
// src/utils/config.ts
|
|
6904
6907
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
6905
6908
|
import { homedir } from "os";
|
|
@@ -7629,6 +7632,15 @@ function createMockClient() {
|
|
|
7629
7632
|
async whoami() {
|
|
7630
7633
|
return mockBackend.whoami();
|
|
7631
7634
|
},
|
|
7635
|
+
async getWorkspaceSettings() {
|
|
7636
|
+
throw new ApiError(501, "workspace settings are not available in mock mode");
|
|
7637
|
+
},
|
|
7638
|
+
async updateWorkspaceSettings() {
|
|
7639
|
+
throw new ApiError(501, "workspace settings are not available in mock mode");
|
|
7640
|
+
},
|
|
7641
|
+
async listBundledAccessClients() {
|
|
7642
|
+
return { items: [] };
|
|
7643
|
+
},
|
|
7632
7644
|
async listProducts() {
|
|
7633
7645
|
return mockBackend.listProducts();
|
|
7634
7646
|
},
|
|
@@ -7776,6 +7788,18 @@ function createMockClient() {
|
|
|
7776
7788
|
async listClientTags() {
|
|
7777
7789
|
return { count: 0, tags: [] };
|
|
7778
7790
|
},
|
|
7791
|
+
async createClientTag() {
|
|
7792
|
+
throw new ApiError(501, "client tag writes are not available in mock mode");
|
|
7793
|
+
},
|
|
7794
|
+
async updateClientTag() {
|
|
7795
|
+
throw new ApiError(501, "client tag writes are not available in mock mode");
|
|
7796
|
+
},
|
|
7797
|
+
async deleteClientTag() {
|
|
7798
|
+
throw new ApiError(501, "client tag writes are not available in mock mode");
|
|
7799
|
+
},
|
|
7800
|
+
async bulkUpdateClientTagAssignments() {
|
|
7801
|
+
throw new ApiError(501, "client tag writes are not available in mock mode");
|
|
7802
|
+
},
|
|
7779
7803
|
async getClientEmailLog() {
|
|
7780
7804
|
throw new ApiError(501, "the email delivery log is not available in mock mode");
|
|
7781
7805
|
},
|
|
@@ -7821,6 +7845,24 @@ function createMockClient() {
|
|
|
7821
7845
|
async listAutomations() {
|
|
7822
7846
|
return { items: [] };
|
|
7823
7847
|
},
|
|
7848
|
+
async getAutomationGraph() {
|
|
7849
|
+
throw new ApiError(501, "workflow authoring is not available in mock mode");
|
|
7850
|
+
},
|
|
7851
|
+
async createAutomation() {
|
|
7852
|
+
throw new ApiError(501, "workflow authoring is not available in mock mode");
|
|
7853
|
+
},
|
|
7854
|
+
async updateAutomationDraft() {
|
|
7855
|
+
throw new ApiError(501, "workflow authoring is not available in mock mode");
|
|
7856
|
+
},
|
|
7857
|
+
async publishAutomation() {
|
|
7858
|
+
throw new ApiError(501, "workflow authoring is not available in mock mode");
|
|
7859
|
+
},
|
|
7860
|
+
async duplicateAutomation() {
|
|
7861
|
+
throw new ApiError(501, "workflow authoring is not available in mock mode");
|
|
7862
|
+
},
|
|
7863
|
+
async deleteAutomation() {
|
|
7864
|
+
throw new ApiError(501, "workflow authoring is not available in mock mode");
|
|
7865
|
+
},
|
|
7824
7866
|
async getAutomation() {
|
|
7825
7867
|
throw new ApiError(501, "workflows are not available in mock mode");
|
|
7826
7868
|
},
|
|
@@ -8207,6 +8249,81 @@ function createMockClient() {
|
|
|
8207
8249
|
async listCampaignRecipients() {
|
|
8208
8250
|
throw new ApiError(501, "email campaigns are not available in mock mode");
|
|
8209
8251
|
},
|
|
8252
|
+
async listCampaignLinks() {
|
|
8253
|
+
throw new ApiError(501, "email campaigns are not available in mock mode");
|
|
8254
|
+
},
|
|
8255
|
+
async listSmsCampaigns() {
|
|
8256
|
+
return { campaigns: [] };
|
|
8257
|
+
},
|
|
8258
|
+
async getSmsCampaign() {
|
|
8259
|
+
throw new ApiError(501, "SMS campaigns are not available in mock mode");
|
|
8260
|
+
},
|
|
8261
|
+
async createSmsCampaign() {
|
|
8262
|
+
throw new ApiError(501, "SMS campaigns are not available in mock mode");
|
|
8263
|
+
},
|
|
8264
|
+
async updateSmsCampaign() {
|
|
8265
|
+
throw new ApiError(501, "SMS campaigns are not available in mock mode");
|
|
8266
|
+
},
|
|
8267
|
+
async deleteSmsCampaign() {
|
|
8268
|
+
throw new ApiError(501, "SMS campaigns are not available in mock mode");
|
|
8269
|
+
},
|
|
8270
|
+
async sendSmsCampaign() {
|
|
8271
|
+
throw new ApiError(501, "SMS campaigns are not available in mock mode");
|
|
8272
|
+
},
|
|
8273
|
+
async cancelSmsCampaign() {
|
|
8274
|
+
throw new ApiError(501, "SMS campaigns are not available in mock mode");
|
|
8275
|
+
},
|
|
8276
|
+
async listSmsCampaignRecipients() {
|
|
8277
|
+
throw new ApiError(501, "SMS campaigns are not available in mock mode");
|
|
8278
|
+
},
|
|
8279
|
+
async listLibraryEmails() {
|
|
8280
|
+
return { emails: [] };
|
|
8281
|
+
},
|
|
8282
|
+
async getLibraryEmail() {
|
|
8283
|
+
throw new ApiError(501, "the e-mail library is not available in mock mode");
|
|
8284
|
+
},
|
|
8285
|
+
async createLibraryEmail() {
|
|
8286
|
+
throw new ApiError(501, "the e-mail library is not available in mock mode");
|
|
8287
|
+
},
|
|
8288
|
+
async updateLibraryEmail() {
|
|
8289
|
+
throw new ApiError(501, "the e-mail library is not available in mock mode");
|
|
8290
|
+
},
|
|
8291
|
+
async duplicateLibraryEmail() {
|
|
8292
|
+
throw new ApiError(501, "the e-mail library is not available in mock mode");
|
|
8293
|
+
},
|
|
8294
|
+
async deleteLibraryEmail() {
|
|
8295
|
+
throw new ApiError(501, "the e-mail library is not available in mock mode");
|
|
8296
|
+
},
|
|
8297
|
+
async getSenderDomain() {
|
|
8298
|
+
throw new ApiError(501, "sender settings are not available in mock mode");
|
|
8299
|
+
},
|
|
8300
|
+
async updateSenderPostalAddress() {
|
|
8301
|
+
throw new ApiError(501, "sender settings are not available in mock mode");
|
|
8302
|
+
},
|
|
8303
|
+
async listWorkspaceClientEmails() {
|
|
8304
|
+
return { emails: [] };
|
|
8305
|
+
},
|
|
8306
|
+
async listClientEmails() {
|
|
8307
|
+
return { emails: [] };
|
|
8308
|
+
},
|
|
8309
|
+
async createClientEmailDraft() {
|
|
8310
|
+
throw new ApiError(501, "client e-mails are not available in mock mode");
|
|
8311
|
+
},
|
|
8312
|
+
async updateClientEmailDraft() {
|
|
8313
|
+
throw new ApiError(501, "client e-mails are not available in mock mode");
|
|
8314
|
+
},
|
|
8315
|
+
async deleteClientEmailDraft() {
|
|
8316
|
+
throw new ApiError(501, "client e-mails are not available in mock mode");
|
|
8317
|
+
},
|
|
8318
|
+
async sendClientEmail() {
|
|
8319
|
+
throw new ApiError(501, "client e-mails are not available in mock mode");
|
|
8320
|
+
},
|
|
8321
|
+
async bulkSetClientsEmailSubscription() {
|
|
8322
|
+
throw new ApiError(501, "bulk subscription changes are not available in mock mode");
|
|
8323
|
+
},
|
|
8324
|
+
async bulkSetClientsSmsSubscription() {
|
|
8325
|
+
throw new ApiError(501, "bulk subscription changes are not available in mock mode");
|
|
8326
|
+
},
|
|
8210
8327
|
async listAudienceSegments() {
|
|
8211
8328
|
return { count: 0, segments: [] };
|
|
8212
8329
|
},
|
|
@@ -8787,6 +8904,9 @@ function createHttpClient(config) {
|
|
|
8787
8904
|
};
|
|
8788
8905
|
return {
|
|
8789
8906
|
whoami: () => request("GET", "/workspace"),
|
|
8907
|
+
getWorkspaceSettings: () => request("GET", "/workspace"),
|
|
8908
|
+
updateWorkspaceSettings: (input) => request("PATCH", "/workspace", input),
|
|
8909
|
+
listBundledAccessClients: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/clients/bundled-access`),
|
|
8790
8910
|
listProducts: () => request("GET", "/products"),
|
|
8791
8911
|
getProduct: (id) => request("GET", `/products/${encodeURIComponent(id)}`),
|
|
8792
8912
|
createProduct: (input) => request("POST", "/products", input),
|
|
@@ -8883,6 +9003,10 @@ function createHttpClient(config) {
|
|
|
8883
9003
|
),
|
|
8884
9004
|
changeClientEmail: (id, newEmail) => request("POST", `/clients/${encodeURIComponent(id)}/change-email`, { newEmail }),
|
|
8885
9005
|
listClientTags: () => request("GET", "/clients/tags"),
|
|
9006
|
+
createClientTag: (input) => request("POST", "/clients/tags", input),
|
|
9007
|
+
updateClientTag: (tagId, input) => request("PATCH", `/clients/tags/${encodeURIComponent(tagId)}`, input),
|
|
9008
|
+
deleteClientTag: (tagId) => request("DELETE", `/clients/tags/${encodeURIComponent(tagId)}`),
|
|
9009
|
+
bulkUpdateClientTagAssignments: (input) => request("POST", "/clients/tags/assignments", input),
|
|
8886
9010
|
getClientEmailLog: (clientId, limit) => request(
|
|
8887
9011
|
"GET",
|
|
8888
9012
|
`/clients/${encodeURIComponent(clientId)}/email-log${limit !== void 0 ? `?limit=${limit}` : ""}`
|
|
@@ -8915,6 +9039,16 @@ function createHttpClient(config) {
|
|
|
8915
9039
|
deleteDiscount: (id) => request("DELETE", `/discounts/${encodeURIComponent(id)}`),
|
|
8916
9040
|
listAutomations: () => request("GET", "/automations"),
|
|
8917
9041
|
getAutomation: (id) => request("GET", `/automations/${encodeURIComponent(id)}`),
|
|
9042
|
+
getAutomationGraph: (id) => request("GET", `/automations/${encodeURIComponent(id)}?include=graph`),
|
|
9043
|
+
createAutomation: (input) => request("POST", "/automations", input),
|
|
9044
|
+
updateAutomationDraft: (id, input) => request("PUT", `/automations/${encodeURIComponent(id)}/draft`, input),
|
|
9045
|
+
publishAutomation: (id) => request("POST", `/automations/${encodeURIComponent(id)}/publish`),
|
|
9046
|
+
duplicateAutomation: (id, name) => request(
|
|
9047
|
+
"POST",
|
|
9048
|
+
`/automations/${encodeURIComponent(id)}/duplicate`,
|
|
9049
|
+
name !== void 0 ? { name } : {}
|
|
9050
|
+
),
|
|
9051
|
+
deleteAutomation: (id) => request("DELETE", `/automations/${encodeURIComponent(id)}`),
|
|
8918
9052
|
listAutomationRuns: (id, limit) => request(
|
|
8919
9053
|
"GET",
|
|
8920
9054
|
`/automations/${encodeURIComponent(id)}/runs${limit !== void 0 ? `?limit=${limit}` : ""}`
|
|
@@ -9244,11 +9378,64 @@ function createHttpClient(config) {
|
|
|
9244
9378
|
listCampaignRecipients: (id, options) => {
|
|
9245
9379
|
const params = new URLSearchParams();
|
|
9246
9380
|
if (options?.status) params.set("status", options.status);
|
|
9381
|
+
if (options?.linkId) params.set("linkId", options.linkId);
|
|
9247
9382
|
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
9248
9383
|
if (options?.cursor) params.set("cursor", options.cursor);
|
|
9249
9384
|
const qs = params.toString();
|
|
9250
9385
|
return request("GET", `/campaigns/${encodeURIComponent(id)}/recipients${qs ? `?${qs}` : ""}`);
|
|
9251
9386
|
},
|
|
9387
|
+
listCampaignLinks: (id) => request("GET", `/campaigns/${encodeURIComponent(id)}/links`),
|
|
9388
|
+
// SMS campaigns — same lifecycle as e-mail campaigns, sent through the
|
|
9389
|
+
// workspace's own SMS provider account.
|
|
9390
|
+
listSmsCampaigns: () => request("GET", "/sms-campaigns"),
|
|
9391
|
+
getSmsCampaign: (id) => request("GET", `/sms-campaigns/${encodeURIComponent(id)}`),
|
|
9392
|
+
createSmsCampaign: (input) => request("POST", "/sms-campaigns", input),
|
|
9393
|
+
updateSmsCampaign: (id, input) => request("PATCH", `/sms-campaigns/${encodeURIComponent(id)}`, input),
|
|
9394
|
+
deleteSmsCampaign: (id) => request("DELETE", `/sms-campaigns/${encodeURIComponent(id)}`),
|
|
9395
|
+
sendSmsCampaign: (id, scheduledAt) => request(
|
|
9396
|
+
"POST",
|
|
9397
|
+
`/sms-campaigns/${encodeURIComponent(id)}/send`,
|
|
9398
|
+
scheduledAt !== void 0 ? { scheduledAt } : {}
|
|
9399
|
+
),
|
|
9400
|
+
cancelSmsCampaign: (id) => request("POST", `/sms-campaigns/${encodeURIComponent(id)}/cancel`),
|
|
9401
|
+
listSmsCampaignRecipients: (id, options) => {
|
|
9402
|
+
const params = new URLSearchParams();
|
|
9403
|
+
if (options?.status) params.set("status", options.status);
|
|
9404
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
9405
|
+
if (options?.cursor) params.set("cursor", options.cursor);
|
|
9406
|
+
const qs = params.toString();
|
|
9407
|
+
return request(
|
|
9408
|
+
"GET",
|
|
9409
|
+
`/sms-campaigns/${encodeURIComponent(id)}/recipients${qs ? `?${qs}` : ""}`
|
|
9410
|
+
);
|
|
9411
|
+
},
|
|
9412
|
+
// Reusable e-mail templates (NOT the system e-mails under /email-templates).
|
|
9413
|
+
listLibraryEmails: () => request("GET", "/email-library"),
|
|
9414
|
+
getLibraryEmail: (id) => request("GET", `/email-library/${encodeURIComponent(id)}`),
|
|
9415
|
+
createLibraryEmail: (input) => request("POST", "/email-library", input),
|
|
9416
|
+
updateLibraryEmail: (id, input) => request("PATCH", `/email-library/${encodeURIComponent(id)}`, input),
|
|
9417
|
+
duplicateLibraryEmail: (id) => request("POST", `/email-library/${encodeURIComponent(id)}/duplicate`),
|
|
9418
|
+
deleteLibraryEmail: (id) => request("DELETE", `/email-library/${encodeURIComponent(id)}`),
|
|
9419
|
+
getSenderDomain: () => request("GET", "/sender-domain"),
|
|
9420
|
+
updateSenderPostalAddress: (postalAddress) => request("PUT", "/sender-domain/postal-address", { postalAddress }),
|
|
9421
|
+
listWorkspaceClientEmails: (limit) => request("GET", `/client-emails${limit !== void 0 ? `?limit=${limit}` : ""}`),
|
|
9422
|
+
listClientEmails: (clientId) => request("GET", `/clients/${encodeURIComponent(clientId)}/emails`),
|
|
9423
|
+
createClientEmailDraft: (clientId, input) => request("POST", `/clients/${encodeURIComponent(clientId)}/emails`, input),
|
|
9424
|
+
updateClientEmailDraft: (clientId, emailId, input) => request(
|
|
9425
|
+
"PATCH",
|
|
9426
|
+
`/clients/${encodeURIComponent(clientId)}/emails/${encodeURIComponent(emailId)}`,
|
|
9427
|
+
input
|
|
9428
|
+
),
|
|
9429
|
+
deleteClientEmailDraft: (clientId, emailId) => request(
|
|
9430
|
+
"DELETE",
|
|
9431
|
+
`/clients/${encodeURIComponent(clientId)}/emails/${encodeURIComponent(emailId)}`
|
|
9432
|
+
),
|
|
9433
|
+
sendClientEmail: (clientId, emailId) => request(
|
|
9434
|
+
"POST",
|
|
9435
|
+
`/clients/${encodeURIComponent(clientId)}/emails/${encodeURIComponent(emailId)}/send`
|
|
9436
|
+
),
|
|
9437
|
+
bulkSetClientsEmailSubscription: (input) => request("POST", "/clients/email-subscription", input),
|
|
9438
|
+
bulkSetClientsSmsSubscription: (input) => request("POST", "/clients/sms-subscription", input),
|
|
9252
9439
|
listAudienceSegments: () => request("GET", "/segments"),
|
|
9253
9440
|
createAudienceSegment: (input) => request("POST", "/segments", input),
|
|
9254
9441
|
updateAudienceSegment: (id, input) => request("PATCH", `/segments/${encodeURIComponent(id)}`, input),
|
|
@@ -9363,7 +9550,10 @@ function createHttpClient(config) {
|
|
|
9363
9550
|
"DELETE",
|
|
9364
9551
|
`/products/${encodeURIComponent(productId)}/kb/articles/${encodeURIComponent(articleId)}`
|
|
9365
9552
|
),
|
|
9366
|
-
addKbCategory: (productId, name) => request("POST", `/products/${encodeURIComponent(productId)}/kb/categories`, {
|
|
9553
|
+
addKbCategory: (productId, name, parentCategoryId) => request("POST", `/products/${encodeURIComponent(productId)}/kb/categories`, {
|
|
9554
|
+
name,
|
|
9555
|
+
...parentCategoryId ? { parentCategoryId } : {}
|
|
9556
|
+
}),
|
|
9367
9557
|
renameKbCategory: (productId, categoryId, name) => request(
|
|
9368
9558
|
"PATCH",
|
|
9369
9559
|
`/products/${encodeURIComponent(productId)}/kb/categories/${encodeURIComponent(categoryId)}`,
|
|
@@ -9821,6 +10011,9 @@ function registerCertificatesCommands(program2) {
|
|
|
9821
10011
|
"--template <value>",
|
|
9822
10012
|
"Explicit template id; omitted = the course template, else the workspace default."
|
|
9823
10013
|
).option(
|
|
10014
|
+
"--reissue-revoked",
|
|
10015
|
+
"Re-issue clients whose current certificate is REVOKED (new document in the current template, new number and verification link, the revoked one stays revoked). ACTIVE certificates are still skipped. Default false = any existing certificate skips the client."
|
|
10016
|
+
).option("--no-reissue-revoked", "Send `reissueRevoked: false` explicitly.").option(
|
|
9824
10017
|
"--confirm",
|
|
9825
10018
|
"required \u2014 acknowledge that you want to issue certificates (recipients get an email)"
|
|
9826
10019
|
).action(async (productId, options, command) => {
|
|
@@ -9832,7 +10025,8 @@ function registerCertificatesCommands(program2) {
|
|
|
9832
10025
|
productId,
|
|
9833
10026
|
clientIds: options.client,
|
|
9834
10027
|
onlyCompleted: options.onlyCompleted,
|
|
9835
|
-
templateId: options.template
|
|
10028
|
+
templateId: options.template,
|
|
10029
|
+
reissueRevoked: options.reissueRevoked
|
|
9836
10030
|
};
|
|
9837
10031
|
const result = await client.call("POST", "/certificates/issue", params, void 0, payload);
|
|
9838
10032
|
emit(flags, result ?? {}, () => printJson(result ?? {}));
|
|
@@ -9872,6 +10066,27 @@ function registerCertificatesCommands(program2) {
|
|
|
9872
10066
|
);
|
|
9873
10067
|
emit(flags, result ?? {}, () => printJson(result ?? {}));
|
|
9874
10068
|
});
|
|
10069
|
+
certificates.command("delete").summary("Delete a revoked certificate").description(
|
|
10070
|
+
'Permanent \u2014 removes a REVOKED certificate together with its PDF; there is NO undo. The public verification link then answers "not found" (instead of "revoked"), and the same client + course can be issued a fresh certificate afterwards. Only revoked certificates can be deleted \u2014 an active one is refused (revoke-certificate first). Addressed by the public credential id (`cert_\u2026`). Meant for test issuances that reached a real client.'
|
|
10071
|
+
).argument("<certificateId>", "Public credential id (`cert_\u2026`) from list-certificates.").option(
|
|
10072
|
+
"--confirm",
|
|
10073
|
+
"required \u2014 acknowledge that you want to delete the revoked certificate (hard delete)"
|
|
10074
|
+
).action(async (certificateId, options, command) => {
|
|
10075
|
+
const flags = getGlobalFlags(command);
|
|
10076
|
+
requireConfirm(options.confirm, "delete the revoked certificate (hard delete)");
|
|
10077
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
10078
|
+
const params = {
|
|
10079
|
+
certificateId
|
|
10080
|
+
};
|
|
10081
|
+
const result = await client.call(
|
|
10082
|
+
"DELETE",
|
|
10083
|
+
"/certificates/:certificateId",
|
|
10084
|
+
params,
|
|
10085
|
+
void 0,
|
|
10086
|
+
void 0
|
|
10087
|
+
);
|
|
10088
|
+
emit(flags, result ?? {}, () => printJson(result ?? {}));
|
|
10089
|
+
});
|
|
9875
10090
|
certificates.command("resend").summary("Resend the certificate email").description(
|
|
9876
10091
|
'Queues the "certificate issued" delivery email (with the PDF link) again \u2014 for bounced or lost mail, or after a recipient-name correction. Active certificates only: a revoked certificate is a 400 (reinstate first), an unknown id a 404. It is a REAL email to the customer. Returns an empty object.'
|
|
9877
10092
|
).argument("<certificateId>", "Public credential id (`cert_\u2026`) from list-certificates.").option("--confirm", "required \u2014 acknowledge that you want to resend the certificate email").action(
|
|
@@ -11217,8 +11432,8 @@ function promptHidden(question) {
|
|
|
11217
11432
|
stdin.setRawMode(true);
|
|
11218
11433
|
stdin.resume();
|
|
11219
11434
|
let value = "";
|
|
11220
|
-
const onData = (
|
|
11221
|
-
for (const char of
|
|
11435
|
+
const onData = (chunk2) => {
|
|
11436
|
+
for (const char of chunk2.toString("utf8")) {
|
|
11222
11437
|
if (char === "\r" || char === "\n") {
|
|
11223
11438
|
stdin.setRawMode(false);
|
|
11224
11439
|
stdin.pause();
|
|
@@ -11722,10 +11937,20 @@ function parsePositiveInt2(value, flag) {
|
|
|
11722
11937
|
}
|
|
11723
11938
|
return n;
|
|
11724
11939
|
}
|
|
11940
|
+
function resolveTemplateId(options) {
|
|
11941
|
+
if (options.templateId === void 0) return void 0;
|
|
11942
|
+
return options.templateId === "none" ? null : options.templateId;
|
|
11943
|
+
}
|
|
11944
|
+
function resolveHtml(options) {
|
|
11945
|
+
return options.htmlFile !== void 0 ? readFileSync4(options.htmlFile, "utf8") : void 0;
|
|
11946
|
+
}
|
|
11725
11947
|
function resolveContent(options) {
|
|
11726
11948
|
if (options.content !== void 0 && options.contentFile !== void 0) {
|
|
11727
11949
|
throw new Error("Use either --content or --content-file, not both.");
|
|
11728
11950
|
}
|
|
11951
|
+
if (options.htmlFile !== void 0 && (options.content !== void 0 || options.contentFile !== void 0)) {
|
|
11952
|
+
throw new Error("Use either --html-file (designed e-mail) or a Markdown body, not both.");
|
|
11953
|
+
}
|
|
11729
11954
|
return options.contentFile !== void 0 ? readFileSync4(options.contentFile, "utf8") : options.content;
|
|
11730
11955
|
}
|
|
11731
11956
|
function resolveSender(options) {
|
|
@@ -11762,6 +11987,26 @@ function resolveAudience(options) {
|
|
|
11762
11987
|
function describeAudience(audience) {
|
|
11763
11988
|
return audience.type === "segment" ? `segment ${audience.segmentId}` : "all subscribed clients";
|
|
11764
11989
|
}
|
|
11990
|
+
function formatRate(value, base) {
|
|
11991
|
+
return base > 0 ? `${(Math.round(value / base * 1e3) / 10).toFixed(1)}%` : "n/a";
|
|
11992
|
+
}
|
|
11993
|
+
function printCampaignEngagement(stats) {
|
|
11994
|
+
const tracked = stats.trackedRecipients ?? 0;
|
|
11995
|
+
const uniqueOpens = stats.uniqueOpens ?? 0;
|
|
11996
|
+
const uniqueClicks = stats.uniqueClicks ?? 0;
|
|
11997
|
+
process.stdout.write(
|
|
11998
|
+
` Engagement (tracked ${tracked} of ${stats.sent}): opens ${uniqueOpens} unique / ${stats.opens ?? 0} total (${formatRate(uniqueOpens, tracked)}, estimate), clicks ${uniqueClicks} unique / ${stats.clicks ?? 0} total (${formatRate(uniqueClicks, tracked)})
|
|
11999
|
+
`
|
|
12000
|
+
);
|
|
12001
|
+
const attributedOrders = stats.attributedOrders ?? 0;
|
|
12002
|
+
if (attributedOrders > 0) {
|
|
12003
|
+
const revenue = ((stats.attributedRevenue ?? 0) / 100).toFixed(2);
|
|
12004
|
+
process.stdout.write(
|
|
12005
|
+
` Attributed: ${attributedOrders} order${attributedOrders === 1 ? "" : "s"}, ${revenue} ${stats.attributedCurrency ?? ""}
|
|
12006
|
+
`
|
|
12007
|
+
);
|
|
12008
|
+
}
|
|
12009
|
+
}
|
|
11765
12010
|
function printCampaignDetail(campaign) {
|
|
11766
12011
|
process.stdout.write(`Campaign ${campaign.id}
|
|
11767
12012
|
`);
|
|
@@ -11794,10 +12039,13 @@ function printCampaignDetail(campaign) {
|
|
|
11794
12039
|
` Stats: sent ${stats.sent}, delivered ${stats.delivered}, bounced ${stats.bounced}, complained ${stats.complained}, failed ${stats.failed}, skipped ${stats.skippedSuppressed + stats.skippedMuted}
|
|
11795
12040
|
`
|
|
11796
12041
|
);
|
|
11797
|
-
|
|
12042
|
+
printCampaignEngagement(stats);
|
|
12043
|
+
const isHtml = campaign.contentFormat === "html";
|
|
12044
|
+
const body = isHtml ? campaign.contentHtml ?? "" : campaign.contentMarkdown;
|
|
12045
|
+
process.stdout.write(` Content (${isHtml ? "html" : "markdown"}, ${body.length} chars):
|
|
11798
12046
|
`);
|
|
11799
|
-
if (
|
|
11800
|
-
for (const line of
|
|
12047
|
+
if (body) {
|
|
12048
|
+
for (const line of body.split("\n")) {
|
|
11801
12049
|
process.stdout.write(` ${line}
|
|
11802
12050
|
`);
|
|
11803
12051
|
}
|
|
@@ -11832,13 +12080,17 @@ function registerCampaignsCommands(program2) {
|
|
|
11832
12080
|
});
|
|
11833
12081
|
campaigns.command("create").description(
|
|
11834
12082
|
"Create a DRAFT campaign \u2014 nothing is emailed; only --name is required at draft time (subject/content/sender are enforced by `campaigns send`)"
|
|
11835
|
-
).requiredOption("--name <name>", "internal label shown in the campaigns list (not the subject)").option("--subject <subject>", "email subject line").option("--preheader <text>", "preview text shown after the subject in inboxes").option("--content <markdown>", "campaign body as Markdown").option("--content-file <path>", "read the campaign body (Markdown) from a file").option(
|
|
12083
|
+
).requiredOption("--name <name>", "internal label shown in the campaigns list (not the subject)").option("--subject <subject>", "email subject line").option("--preheader <text>", "preview text shown after the subject in inboxes").option("--content <markdown>", "campaign body as Markdown").option("--content-file <path>", "read the campaign body (Markdown) from a file").option(
|
|
12084
|
+
"--html-file <path>",
|
|
12085
|
+
"create a designed e-mail from a standalone HTML file (instead of a Markdown body)"
|
|
12086
|
+
).option("--template-id <id>", "look template the Markdown body renders into").option("--from-name <name>", "From display name").option("--reply-to <email>", "Reply-To address (requires --from-name)").option(
|
|
11836
12087
|
"--from-email <email>",
|
|
11837
12088
|
"custom From on the workspace's verified sending domain (requires --from-name); omit for the platform sender"
|
|
11838
12089
|
).option("--segment <segmentId>", "send to a saved audience segment (re-resolved at send time)").option("--all-subscribed", "send to every subscribed client (the default audience)").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").action(
|
|
11839
12090
|
async (options, command) => {
|
|
11840
12091
|
const flags = getGlobalFlags(command);
|
|
11841
12092
|
const contentMarkdown = resolveContent(options);
|
|
12093
|
+
const contentHtml = resolveHtml(options);
|
|
11842
12094
|
const sender = resolveSender(options);
|
|
11843
12095
|
const audience = resolveAudience(options);
|
|
11844
12096
|
const theme = resolveTheme(options);
|
|
@@ -11848,6 +12100,8 @@ function registerCampaignsCommands(program2) {
|
|
|
11848
12100
|
subject: options.subject,
|
|
11849
12101
|
preheader: options.preheader,
|
|
11850
12102
|
contentMarkdown,
|
|
12103
|
+
...contentHtml !== void 0 ? { contentFormat: "html", contentHtml } : {},
|
|
12104
|
+
templateId: resolveTemplateId(options),
|
|
11851
12105
|
sender,
|
|
11852
12106
|
audience,
|
|
11853
12107
|
theme
|
|
@@ -11860,7 +12114,7 @@ function registerCampaignsCommands(program2) {
|
|
|
11860
12114
|
});
|
|
11861
12115
|
}
|
|
11862
12116
|
);
|
|
11863
|
-
campaigns.command("update").description("Update a DRAFT campaign (campaigns past draft are immutable except cancel)").argument("<id>", "campaign id").option("--name <name>", "new internal label").option("--subject <subject>", "new email subject line").option("--preheader <text>", "new preview text").option("--content <markdown>", "new campaign body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new campaign body (Markdown) from a file").option("--from-name <name>", "new From display name").option("--reply-to <email>", "new Reply-To address (requires --from-name)").option("--from-email <email>", "new custom From address (requires --from-name)").option("--segment <segmentId>", "switch the audience to a saved segment").option("--all-subscribed", "switch the audience to every subscribed client").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").action(
|
|
12117
|
+
campaigns.command("update").description("Update a DRAFT campaign (campaigns past draft are immutable except cancel)").argument("<id>", "campaign id").option("--name <name>", "new internal label").option("--subject <subject>", "new email subject line").option("--preheader <text>", "new preview text").option("--content <markdown>", "new campaign body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new campaign body (Markdown) from a file").option("--html-file <path>", "read the new HTML document of a designed e-mail from a file").option("--template-id <id>", "look template for the body (`none` = the default shell)").option("--from-name <name>", "new From display name").option("--reply-to <email>", "new Reply-To address (requires --from-name)").option("--from-email <email>", "new custom From address (requires --from-name)").option("--segment <segmentId>", "switch the audience to a saved segment").option("--all-subscribed", "switch the audience to every subscribed client").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").action(
|
|
11864
12118
|
async (id, options, command) => {
|
|
11865
12119
|
const flags = getGlobalFlags(command);
|
|
11866
12120
|
const input = {};
|
|
@@ -11869,6 +12123,10 @@ function registerCampaignsCommands(program2) {
|
|
|
11869
12123
|
if (options.preheader !== void 0) input.preheader = options.preheader;
|
|
11870
12124
|
const contentMarkdown = resolveContent(options);
|
|
11871
12125
|
if (contentMarkdown !== void 0) input.contentMarkdown = contentMarkdown;
|
|
12126
|
+
const contentHtml = resolveHtml(options);
|
|
12127
|
+
if (contentHtml !== void 0) input.contentHtml = contentHtml;
|
|
12128
|
+
const templateId = resolveTemplateId(options);
|
|
12129
|
+
if (templateId !== void 0) input.templateId = templateId;
|
|
11872
12130
|
const sender = resolveSender(options);
|
|
11873
12131
|
if (sender !== void 0) input.sender = sender;
|
|
11874
12132
|
const audience = resolveAudience(options);
|
|
@@ -11947,7 +12205,12 @@ function registerCampaignsCommands(program2) {
|
|
|
11947
12205
|
);
|
|
11948
12206
|
});
|
|
11949
12207
|
});
|
|
11950
|
-
campaigns.command("recipients").description(
|
|
12208
|
+
campaigns.command("recipients").description(
|
|
12209
|
+
"List a campaign's recipients (report drill-down) with per-recipient opens/clicks, optionally by status or by clicked link"
|
|
12210
|
+
).argument("<id>", "campaign id").option("--status <status>", `filter to one status (${RECIPIENT_STATUSES.join(" | ")})`).option(
|
|
12211
|
+
"--link <linkId>",
|
|
12212
|
+
"only recipients who clicked this link (id from `campaigns links`); wins over --status"
|
|
12213
|
+
).option("--limit <n>", "page size").option("--cursor <cursor>", "cursor from a previous page").action(
|
|
11951
12214
|
async (id, options, command) => {
|
|
11952
12215
|
const flags = getGlobalFlags(command);
|
|
11953
12216
|
let status;
|
|
@@ -11963,6 +12226,7 @@ function registerCampaignsCommands(program2) {
|
|
|
11963
12226
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
11964
12227
|
const page = await client.listCampaignRecipients(id, {
|
|
11965
12228
|
status,
|
|
12229
|
+
linkId: options.link,
|
|
11966
12230
|
limit: options.limit !== void 0 ? parsePositiveInt2(options.limit, "--limit") : void 0,
|
|
11967
12231
|
cursor: options.cursor
|
|
11968
12232
|
});
|
|
@@ -11972,10 +12236,13 @@ function registerCampaignsCommands(program2) {
|
|
|
11972
12236
|
id: r.id,
|
|
11973
12237
|
email: r.email,
|
|
11974
12238
|
status: r.status,
|
|
12239
|
+
// "-" = not measured (opt-out or tracking off), distinct from 0.
|
|
12240
|
+
opens: r.tracked ? r.openCount : "-",
|
|
12241
|
+
clicks: r.tracked ? r.clickCount : "-",
|
|
11975
12242
|
processedAt: r.processedAt ?? "",
|
|
11976
12243
|
details: r.statusDetails ?? ""
|
|
11977
12244
|
})),
|
|
11978
|
-
["id", "email", "status", "processedAt", "details"]
|
|
12245
|
+
["id", "email", "status", "opens", "clicks", "processedAt", "details"]
|
|
11979
12246
|
);
|
|
11980
12247
|
if (page.nextCursor) {
|
|
11981
12248
|
process.stdout.write(`
|
|
@@ -11985,6 +12252,30 @@ Next page: --cursor ${page.nextCursor}
|
|
|
11985
12252
|
});
|
|
11986
12253
|
}
|
|
11987
12254
|
);
|
|
12255
|
+
campaigns.command("links").description("A sent campaign's link table: per-link unique/total clicks, most-clicked first").argument("<id>", "campaign id").action(async (id, _options, command) => {
|
|
12256
|
+
const flags = getGlobalFlags(command);
|
|
12257
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12258
|
+
const result = await client.listCampaignLinks(id);
|
|
12259
|
+
emit(flags, result, () => {
|
|
12260
|
+
if (result.links.length === 0) {
|
|
12261
|
+
process.stdout.write(
|
|
12262
|
+
"No clicks recorded yet (or the campaign was sent with tracking off).\n"
|
|
12263
|
+
);
|
|
12264
|
+
return;
|
|
12265
|
+
}
|
|
12266
|
+
printTable(
|
|
12267
|
+
result.links.map((link) => ({
|
|
12268
|
+
id: link.id,
|
|
12269
|
+
url: link.url,
|
|
12270
|
+
uniqueClicks: link.uniqueClicks,
|
|
12271
|
+
clicks: link.clicks,
|
|
12272
|
+
lastClickedAt: link.lastClickedAt
|
|
12273
|
+
})),
|
|
12274
|
+
["id", "url", "uniqueClicks", "clicks", "lastClickedAt"]
|
|
12275
|
+
);
|
|
12276
|
+
process.stdout.write("\nWho clicked a link: campaigns recipients <id> --link <linkId>\n");
|
|
12277
|
+
});
|
|
12278
|
+
});
|
|
11988
12279
|
}
|
|
11989
12280
|
|
|
11990
12281
|
// src/commands/checkoutSettings/index.ts
|
|
@@ -12229,7 +12520,7 @@ function buildConsentInput(options) {
|
|
|
12229
12520
|
}
|
|
12230
12521
|
|
|
12231
12522
|
// src/commands/clients/index.ts
|
|
12232
|
-
import { readFileSync as
|
|
12523
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
12233
12524
|
|
|
12234
12525
|
// src/client/types.ts
|
|
12235
12526
|
var AIImageAspectRatio = /* @__PURE__ */ ((AIImageAspectRatio2) => {
|
|
@@ -12283,66 +12574,505 @@ var PORTAL_LIBRARY_ORDERS = [
|
|
|
12283
12574
|
"custom"
|
|
12284
12575
|
];
|
|
12285
12576
|
var CUSTOMER_ACCESS_REPLACEMENT_STRATEGIES = ["adaptive", "always_allow", "creator_approval_only"];
|
|
12577
|
+
var CLIENT_TAG_COLORS = [
|
|
12578
|
+
"magenta",
|
|
12579
|
+
"red",
|
|
12580
|
+
"volcano",
|
|
12581
|
+
"orange",
|
|
12582
|
+
"gold",
|
|
12583
|
+
"lime",
|
|
12584
|
+
"green",
|
|
12585
|
+
"cyan",
|
|
12586
|
+
"blue",
|
|
12587
|
+
"geekblue",
|
|
12588
|
+
"purple"
|
|
12589
|
+
];
|
|
12590
|
+
var TRANSCRIPTION_LANGUAGES = ["pl", "en", "default"];
|
|
12286
12591
|
|
|
12287
|
-
// src/commands/clients/
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
12592
|
+
// src/commands/clients/emails.ts
|
|
12593
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
12594
|
+
function parsePositiveInt3(value, flag) {
|
|
12595
|
+
const n = Number.parseInt(value, 10);
|
|
12596
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
12597
|
+
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
12294
12598
|
}
|
|
12599
|
+
return n;
|
|
12295
12600
|
}
|
|
12296
|
-
function
|
|
12297
|
-
if (
|
|
12601
|
+
function resolveContent2(options) {
|
|
12602
|
+
if (options.content !== void 0 && options.contentFile !== void 0) {
|
|
12603
|
+
throw new Error("Use either --content or --content-file, not both.");
|
|
12604
|
+
}
|
|
12605
|
+
return options.contentFile !== void 0 ? readFileSync6(options.contentFile, "utf8") : options.content;
|
|
12606
|
+
}
|
|
12607
|
+
function resolveSender2(options) {
|
|
12608
|
+
if (options.fromName === void 0 && options.replyTo === void 0 && options.fromEmail === void 0) {
|
|
12609
|
+
return void 0;
|
|
12610
|
+
}
|
|
12611
|
+
return {
|
|
12612
|
+
...options.fromName !== void 0 ? { fromName: options.fromName } : {},
|
|
12613
|
+
...options.replyTo !== void 0 ? { replyTo: options.replyTo } : {},
|
|
12614
|
+
...options.fromEmail !== void 0 ? { fromEmail: options.fromEmail } : {}
|
|
12615
|
+
};
|
|
12616
|
+
}
|
|
12617
|
+
function resolveBranded(options) {
|
|
12618
|
+
if (options.branded && options.plain) {
|
|
12619
|
+
throw new Error("Use either --branded or --plain, not both.");
|
|
12620
|
+
}
|
|
12621
|
+
if (options.branded) return true;
|
|
12622
|
+
if (options.plain) return false;
|
|
12623
|
+
return void 0;
|
|
12624
|
+
}
|
|
12625
|
+
function printEmails(emails) {
|
|
12626
|
+
printTable(
|
|
12627
|
+
emails.map((e) => ({
|
|
12628
|
+
id: e.id,
|
|
12629
|
+
clientId: e.clientId,
|
|
12630
|
+
to: e.recipientEmail,
|
|
12631
|
+
status: e.status,
|
|
12632
|
+
subject: e.subject,
|
|
12633
|
+
sentAt: e.sentAt ?? "",
|
|
12634
|
+
updatedAt: e.updatedAt
|
|
12635
|
+
})),
|
|
12636
|
+
["id", "clientId", "to", "status", "subject", "sentAt", "updatedAt"]
|
|
12637
|
+
);
|
|
12638
|
+
}
|
|
12639
|
+
function printEmailDetail(email) {
|
|
12640
|
+
process.stdout.write(`Client e-mail ${email.id} (${email.status})
|
|
12641
|
+
`);
|
|
12642
|
+
process.stdout.write(
|
|
12643
|
+
` To: ${email.recipientName ? `${email.recipientName} ` : ""}<${email.recipientEmail}> (client ${email.clientId})
|
|
12644
|
+
`
|
|
12645
|
+
);
|
|
12646
|
+
process.stdout.write(
|
|
12647
|
+
` From: ${email.sender.fromName}${email.sender.fromEmail ? ` <${email.sender.fromEmail}>` : " (platform sender)"}${email.sender.replyTo ? `, reply-to ${email.sender.replyTo}` : ""}
|
|
12648
|
+
`
|
|
12649
|
+
);
|
|
12650
|
+
process.stdout.write(` Subject: ${email.subject}
|
|
12651
|
+
`);
|
|
12652
|
+
process.stdout.write(` Frame: ${email.branded ? "branded" : "plain"}
|
|
12653
|
+
`);
|
|
12654
|
+
if (email.sentAt) process.stdout.write(` Sent at: ${email.sentAt}
|
|
12655
|
+
`);
|
|
12656
|
+
if (email.failureReason) process.stdout.write(` Failure: ${email.failureReason}
|
|
12657
|
+
`);
|
|
12658
|
+
}
|
|
12659
|
+
var SEND_WARNING = "delivers a REAL e-mail to the customer and cannot be recalled once it is out";
|
|
12660
|
+
var SEND_FAILED_STATUS = 502;
|
|
12661
|
+
function withSendFailedHint(error) {
|
|
12662
|
+
if (error instanceof ApiError && error.status === SEND_FAILED_STATUS) {
|
|
12298
12663
|
throw new ApiError(
|
|
12299
12664
|
error.status,
|
|
12300
|
-
|
|
12301
|
-
"The
|
|
12665
|
+
error.apiMessage,
|
|
12666
|
+
"The send did not complete. Check the status with `clients emails list` \u2014 `failed` means nothing went out and it can be sent again. Re-running `clients emails send` is safe: the same draft is never delivered twice."
|
|
12302
12667
|
);
|
|
12303
12668
|
}
|
|
12304
12669
|
throw error;
|
|
12305
12670
|
}
|
|
12306
|
-
function
|
|
12307
|
-
const
|
|
12671
|
+
function registerClientEmailsCommands(clients2) {
|
|
12672
|
+
const emails = clients2.command("emails").description(
|
|
12673
|
+
"One-off, personal e-mails from you to ONE customer (the customer card \u2192 E-mail). Drafts are harmless; `send` e-mails the customer for real"
|
|
12674
|
+
);
|
|
12675
|
+
emails.command("inbox").description("The workspace-wide feed: drafts + sent e-mails across every customer").option("--limit <n>", "max entries (default 200, max 500)").action(async (options, command) => {
|
|
12676
|
+
const flags = getGlobalFlags(command);
|
|
12677
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12678
|
+
const page = await client.listWorkspaceClientEmails(
|
|
12679
|
+
options.limit !== void 0 ? parsePositiveInt3(options.limit, "--limit") : void 0
|
|
12680
|
+
);
|
|
12681
|
+
emit(flags, page, () => printEmails(page.emails));
|
|
12682
|
+
});
|
|
12683
|
+
emails.command("list").description("One customer's e-mails (drafts + sent), newest first").argument("<clientId>", "workspace client id (wcid)").action(async (clientId, _options, command) => {
|
|
12684
|
+
const flags = getGlobalFlags(command);
|
|
12685
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12686
|
+
const page = await client.listClientEmails(clientId);
|
|
12687
|
+
emit(flags, page, () => printEmails(page.emails));
|
|
12688
|
+
});
|
|
12689
|
+
emails.command("draft").description(
|
|
12690
|
+
"Create a DRAFT e-mail to one customer \u2014 nothing is sent until `clients emails send`"
|
|
12691
|
+
).argument("<clientId>", "workspace client id (wcid)").requiredOption("--subject <subject>", "subject line (max 200 chars)").option("--content <markdown>", "e-mail body as Markdown").option("--content-file <path>", "read the e-mail body (Markdown) from a file").option("--from-name <name>", "From display name (default: the workspace sender)").option("--reply-to <email>", "Reply-To address").option("--from-email <email>", "From address on the workspace's verified sending domain").option("--branded", "wrap it in the workspace's branded frame").option("--plain", "a plain, personal-looking e-mail (the default)").action(
|
|
12692
|
+
async (clientId, options, command) => {
|
|
12693
|
+
const flags = getGlobalFlags(command);
|
|
12694
|
+
const contentMarkdown = resolveContent2(options);
|
|
12695
|
+
if (contentMarkdown === void 0) {
|
|
12696
|
+
throw new Error("Pass the body with --content or --content-file.");
|
|
12697
|
+
}
|
|
12698
|
+
const sender = resolveSender2(options);
|
|
12699
|
+
const branded = resolveBranded(options);
|
|
12700
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12701
|
+
const result = await client.createClientEmailDraft(clientId, {
|
|
12702
|
+
subject: options.subject,
|
|
12703
|
+
contentMarkdown,
|
|
12704
|
+
...sender !== void 0 ? { sender } : {},
|
|
12705
|
+
...branded !== void 0 ? { branded } : {}
|
|
12706
|
+
});
|
|
12707
|
+
emit(flags, result, () => {
|
|
12708
|
+
printEmailDetail(result.email);
|
|
12709
|
+
process.stdout.write(
|
|
12710
|
+
`Nothing is sent yet \u2014 send it with \`clients emails send ${clientId} ${result.email.id} --confirm\`.
|
|
12711
|
+
`
|
|
12712
|
+
);
|
|
12713
|
+
});
|
|
12714
|
+
}
|
|
12715
|
+
);
|
|
12716
|
+
emails.command("update").description("Edit a DRAFT (a sent e-mail is a record and cannot be edited)").argument("<clientId>", "workspace client id (wcid)").argument("<emailId>", "e-mail id (from `clients emails list`)").option("--subject <subject>", "new subject line").option("--content <markdown>", "new body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new body (Markdown) from a file").option("--from-name <name>", "new From display name").option("--reply-to <email>", "new Reply-To address").option("--from-email <email>", "new From address on the verified sending domain").option("--branded", "wrap it in the workspace's branded frame").option("--plain", "a plain, personal-looking e-mail").action(
|
|
12717
|
+
async (clientId, emailId, options, command) => {
|
|
12718
|
+
const flags = getGlobalFlags(command);
|
|
12719
|
+
const input = {};
|
|
12720
|
+
if (options.subject !== void 0) input.subject = options.subject;
|
|
12721
|
+
const contentMarkdown = resolveContent2(options);
|
|
12722
|
+
if (contentMarkdown !== void 0) input.contentMarkdown = contentMarkdown;
|
|
12723
|
+
const sender = resolveSender2(options);
|
|
12724
|
+
if (sender !== void 0) input.sender = sender;
|
|
12725
|
+
const branded = resolveBranded(options);
|
|
12726
|
+
if (branded !== void 0) input.branded = branded;
|
|
12727
|
+
if (Object.keys(input).length === 0) {
|
|
12728
|
+
throw new Error(
|
|
12729
|
+
"Nothing to update \u2014 pass at least one change flag (see `clients emails update --help`)."
|
|
12730
|
+
);
|
|
12731
|
+
}
|
|
12732
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12733
|
+
const result = await client.updateClientEmailDraft(clientId, emailId, input);
|
|
12734
|
+
emit(flags, result, () => printEmailDetail(result.email));
|
|
12735
|
+
}
|
|
12736
|
+
);
|
|
12737
|
+
emails.command("delete").description("Delete a DRAFT (sent e-mails stay as a record). Requires --confirm").argument("<clientId>", "workspace client id (wcid)").argument("<emailId>", "e-mail id").option("--confirm", "required \u2014 acknowledge the deletion").action(
|
|
12738
|
+
async (clientId, emailId, options, command) => {
|
|
12739
|
+
const flags = getGlobalFlags(command);
|
|
12740
|
+
if (!options.confirm) {
|
|
12741
|
+
throw new Error(
|
|
12742
|
+
`Refusing to delete draft ${emailId} without --confirm. Re-run with --confirm to proceed.`
|
|
12743
|
+
);
|
|
12744
|
+
}
|
|
12745
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12746
|
+
const result = await client.deleteClientEmailDraft(clientId, emailId);
|
|
12747
|
+
emit(flags, result, () => {
|
|
12748
|
+
process.stdout.write(`Deleted draft ${emailId}.
|
|
12749
|
+
`);
|
|
12750
|
+
});
|
|
12751
|
+
}
|
|
12752
|
+
);
|
|
12753
|
+
emails.command("send").description(`\u26A0\uFE0F SENDS the draft \u2014 ${SEND_WARNING}. Requires --confirm`).argument("<clientId>", "workspace client id (wcid)").argument("<emailId>", "e-mail id (a draft, or a failed send to retry)").option("--confirm", "required \u2014 acknowledge this e-mails the customer for real").action(
|
|
12754
|
+
async (clientId, emailId, options, command) => {
|
|
12755
|
+
const flags = getGlobalFlags(command);
|
|
12756
|
+
if (!options.confirm) {
|
|
12757
|
+
throw new Error(
|
|
12758
|
+
`Refusing to send without --confirm. \`clients emails send\` ${SEND_WARNING}. Check the draft first with \`clients emails list ${clientId}\`, then re-run with --confirm.`
|
|
12759
|
+
);
|
|
12760
|
+
}
|
|
12761
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12762
|
+
const result = await client.sendClientEmail(clientId, emailId).catch(withSendFailedHint);
|
|
12763
|
+
emit(flags, result, () => {
|
|
12764
|
+
process.stdout.write(
|
|
12765
|
+
result.email.status === "sent" ? `Sent "${result.email.subject}" to ${result.email.recipientEmail}.
|
|
12766
|
+
` : `E-mail ${result.email.id} is ${result.email.status}${result.email.failureReason ? ` \u2014 ${result.email.failureReason}` : ""}.
|
|
12767
|
+
`
|
|
12768
|
+
);
|
|
12769
|
+
});
|
|
12770
|
+
}
|
|
12771
|
+
);
|
|
12772
|
+
}
|
|
12773
|
+
|
|
12774
|
+
// src/commands/clients/subscription.ts
|
|
12775
|
+
var MAX_SUBSCRIPTION_CLIENTS_PER_REQUEST = 200;
|
|
12776
|
+
var MAX_REASON_NOTE_LENGTH = 500;
|
|
12777
|
+
var SUBSCRIPTION_STATUSES = ["subscribed", "unsubscribed"];
|
|
12778
|
+
var EMAIL_REASONS = [
|
|
12779
|
+
"imported-from-other-list",
|
|
12780
|
+
"existing-customers-consent",
|
|
12781
|
+
"asked-directly",
|
|
12782
|
+
"other"
|
|
12783
|
+
];
|
|
12784
|
+
var SMS_REASONS = [
|
|
12785
|
+
"explicit-sms-consent",
|
|
12786
|
+
"asked-directly",
|
|
12787
|
+
"other"
|
|
12788
|
+
];
|
|
12789
|
+
function parseIds(value) {
|
|
12790
|
+
const ids = [
|
|
12308
12791
|
...new Set(
|
|
12309
|
-
value.split(",").map((item) => item.trim()).filter(
|
|
12792
|
+
value.split(",").map((item) => item.trim()).filter((item) => item.length > 0)
|
|
12310
12793
|
)
|
|
12311
12794
|
];
|
|
12312
|
-
if (
|
|
12313
|
-
|
|
12314
|
-
}
|
|
12315
|
-
return items;
|
|
12795
|
+
if (ids.length === 0) throw new Error("--id needs at least one client id.");
|
|
12796
|
+
return ids;
|
|
12316
12797
|
}
|
|
12317
|
-
function
|
|
12318
|
-
|
|
12319
|
-
|
|
12798
|
+
function parseStatus(value) {
|
|
12799
|
+
const match = SUBSCRIPTION_STATUSES.find((s) => s === value);
|
|
12800
|
+
if (!match) {
|
|
12801
|
+
throw new Error(
|
|
12802
|
+
`Invalid --status: ${value} (expected one of ${SUBSCRIPTION_STATUSES.join(", ")})`
|
|
12803
|
+
);
|
|
12320
12804
|
}
|
|
12321
|
-
|
|
12322
|
-
return { clientEmails: parseCommaList2(options.email ?? "", "--email"), processAllClients: false };
|
|
12323
|
-
}
|
|
12324
|
-
function describeBulkSelection(options) {
|
|
12325
|
-
return options.all ? "ALL active clients" : `client(s) ${options.email ?? ""}`;
|
|
12805
|
+
return match;
|
|
12326
12806
|
}
|
|
12327
|
-
function
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
throw new Error(
|
|
12332
|
-
`Invalid --columns entry: ${column} (expected ${CLIENTS_EXPORT_COLUMN_GROUPS.join(", ")})`
|
|
12333
|
-
);
|
|
12807
|
+
function parseReason(options, status, reasons) {
|
|
12808
|
+
if (status === "unsubscribed") {
|
|
12809
|
+
if (options.reason !== void 0 || options.reasonNote !== void 0) {
|
|
12810
|
+
throw new Error("--reason / --reason-note only apply to --status subscribed.");
|
|
12334
12811
|
}
|
|
12335
|
-
return
|
|
12336
|
-
}
|
|
12812
|
+
return void 0;
|
|
12813
|
+
}
|
|
12814
|
+
const match = reasons.find((r) => r === options.reason);
|
|
12815
|
+
if (!match) {
|
|
12816
|
+
throw new Error(
|
|
12817
|
+
`--status subscribed needs --reason (the consent basis you attest to): one of ${reasons.join(", ")}`
|
|
12818
|
+
);
|
|
12819
|
+
}
|
|
12820
|
+
if (options.reasonNote !== void 0 && options.reasonNote.length > MAX_REASON_NOTE_LENGTH) {
|
|
12821
|
+
throw new Error(`--reason-note is limited to ${MAX_REASON_NOTE_LENGTH} characters.`);
|
|
12822
|
+
}
|
|
12823
|
+
return match;
|
|
12337
12824
|
}
|
|
12338
|
-
function
|
|
12339
|
-
if (
|
|
12340
|
-
throw new Error(
|
|
12825
|
+
function requireConfirm3(options, what) {
|
|
12826
|
+
if (!options.confirm) {
|
|
12827
|
+
throw new Error(
|
|
12828
|
+
`Refusing to ${what} without --confirm \u2014 this changes marketing consent records. Re-run with --confirm to proceed.`
|
|
12829
|
+
);
|
|
12830
|
+
}
|
|
12831
|
+
}
|
|
12832
|
+
function chunk(ids) {
|
|
12833
|
+
const chunks = [];
|
|
12834
|
+
for (let offset = 0; offset < ids.length; offset += MAX_SUBSCRIPTION_CLIENTS_PER_REQUEST) {
|
|
12835
|
+
chunks.push(ids.slice(offset, offset + MAX_SUBSCRIPTION_CLIENTS_PER_REQUEST));
|
|
12836
|
+
}
|
|
12837
|
+
return chunks;
|
|
12838
|
+
}
|
|
12839
|
+
function mergeResults(pages) {
|
|
12840
|
+
return {
|
|
12841
|
+
updatedClientIds: pages.flatMap((p) => p.updatedClientIds),
|
|
12842
|
+
unchangedClientIds: pages.flatMap((p) => p.unchangedClientIds),
|
|
12843
|
+
skippedClientIds: pages.flatMap((p) => p.skippedClientIds)
|
|
12844
|
+
};
|
|
12845
|
+
}
|
|
12846
|
+
function printSummary(channel, status, summary) {
|
|
12847
|
+
process.stdout.write(
|
|
12848
|
+
`${channel}: ${summary.updatedClientIds.length} set to ${status}, ${summary.unchangedClientIds.length} already ${status}, ${summary.skippedClientIds.length} skipped (missing / merged / deleted).
|
|
12849
|
+
`
|
|
12850
|
+
);
|
|
12851
|
+
}
|
|
12852
|
+
function registerClientSubscriptionCommands(clients2) {
|
|
12853
|
+
clients2.command("email-subscription").description(
|
|
12854
|
+
`Subscribe / unsubscribe MANY clients to marketing e-mail at once. Subscribing records the consent basis YOU attest to (--reason). Sent in batches of ${MAX_SUBSCRIPTION_CLIENTS_PER_REQUEST}. Requires --confirm`
|
|
12855
|
+
).requiredOption("--id <ids>", "comma-separated workspace client ids (wcid)").requiredOption("--status <status>", SUBSCRIPTION_STATUSES.join(" | ")).option("--reason <reason>", `consent basis when subscribing (${EMAIL_REASONS.join(" | ")})`).option("--reason-note <text>", "optional detail for --reason").option("--confirm", "required \u2014 acknowledge the consent change").action(async (options, command) => {
|
|
12856
|
+
const flags = getGlobalFlags(command);
|
|
12857
|
+
const clientIds = parseIds(options.id);
|
|
12858
|
+
const status = parseStatus(options.status);
|
|
12859
|
+
const reason = parseReason(options, status, EMAIL_REASONS);
|
|
12860
|
+
requireConfirm3(options, `set ${clientIds.length} client(s) to e-mail ${status}`);
|
|
12861
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12862
|
+
const pages = [];
|
|
12863
|
+
for (const ids of chunk(clientIds)) {
|
|
12864
|
+
pages.push(
|
|
12865
|
+
await client.bulkSetClientsEmailSubscription(
|
|
12866
|
+
reason !== void 0 ? { clientIds: ids, status: "subscribed", reason, reasonNote: options.reasonNote } : { clientIds: ids, status: "unsubscribed" }
|
|
12867
|
+
)
|
|
12868
|
+
);
|
|
12869
|
+
}
|
|
12870
|
+
const summary = mergeResults(pages);
|
|
12871
|
+
emit(flags, summary, () => printSummary("Marketing e-mail", status, summary));
|
|
12872
|
+
});
|
|
12873
|
+
clients2.command("sms-subscription").description(
|
|
12874
|
+
`Subscribe / unsubscribe MANY clients to marketing SMS at once. SMS is its own consent \u2014 an e-mail opt-in never covers it \u2014 so subscribing needs an SMS-specific --reason. Sent in batches of ${MAX_SUBSCRIPTION_CLIENTS_PER_REQUEST}. Requires --confirm`
|
|
12875
|
+
).requiredOption("--id <ids>", "comma-separated workspace client ids (wcid)").requiredOption("--status <status>", SUBSCRIPTION_STATUSES.join(" | ")).option("--reason <reason>", `consent basis when subscribing (${SMS_REASONS.join(" | ")})`).option("--reason-note <text>", "optional detail for --reason").option("--confirm", "required \u2014 acknowledge the consent change").action(async (options, command) => {
|
|
12876
|
+
const flags = getGlobalFlags(command);
|
|
12877
|
+
const clientIds = parseIds(options.id);
|
|
12878
|
+
const status = parseStatus(options.status);
|
|
12879
|
+
const reason = parseReason(options, status, SMS_REASONS);
|
|
12880
|
+
requireConfirm3(options, `set ${clientIds.length} client(s) to SMS ${status}`);
|
|
12881
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12882
|
+
const pages = [];
|
|
12883
|
+
for (const ids of chunk(clientIds)) {
|
|
12884
|
+
pages.push(
|
|
12885
|
+
await client.bulkSetClientsSmsSubscription(
|
|
12886
|
+
reason !== void 0 ? { clientIds: ids, status: "subscribed", reason, reasonNote: options.reasonNote } : { clientIds: ids, status: "unsubscribed" }
|
|
12887
|
+
)
|
|
12888
|
+
);
|
|
12889
|
+
}
|
|
12890
|
+
const summary = mergeResults(pages);
|
|
12891
|
+
emit(flags, summary, () => printSummary("Marketing SMS", status, summary));
|
|
12892
|
+
});
|
|
12893
|
+
}
|
|
12894
|
+
|
|
12895
|
+
// src/commands/clients/tags.ts
|
|
12896
|
+
var MAX_TAG_CLIENTS_PER_REQUEST = 200;
|
|
12897
|
+
function parseCommaList2(value, flag) {
|
|
12898
|
+
const items = [
|
|
12899
|
+
...new Set(
|
|
12900
|
+
value.split(",").map((item) => item.trim()).filter((item) => item.length > 0)
|
|
12901
|
+
)
|
|
12902
|
+
];
|
|
12903
|
+
if (items.length === 0) throw new Error(`${flag} needs at least one value.`);
|
|
12904
|
+
return items;
|
|
12905
|
+
}
|
|
12906
|
+
function parseColor(value) {
|
|
12907
|
+
const match = CLIENT_TAG_COLORS.find((color) => color === value);
|
|
12908
|
+
if (!match) {
|
|
12909
|
+
throw new Error(`Invalid --color: ${value} (expected one of ${CLIENT_TAG_COLORS.join(", ")})`);
|
|
12910
|
+
}
|
|
12911
|
+
return match;
|
|
12912
|
+
}
|
|
12913
|
+
function resolveColor(options) {
|
|
12914
|
+
if (options.color !== void 0 && options.clearColor) {
|
|
12915
|
+
throw new Error("Use either --color or --clear-color, not both.");
|
|
12916
|
+
}
|
|
12917
|
+
if (options.clearColor) return null;
|
|
12918
|
+
return options.color !== void 0 ? parseColor(options.color) : void 0;
|
|
12919
|
+
}
|
|
12920
|
+
function registerClientTagsCommands(clients2) {
|
|
12921
|
+
const tags = clients2.command("tags").description(
|
|
12922
|
+
"Workspace client tags (Customers \u2192 Tags): the registry that segments, campaign link actions and imports refer to by id \u2014 list, create, rename / recolour, delete, assign"
|
|
12923
|
+
);
|
|
12924
|
+
tags.command("list", { isDefault: true }).description(
|
|
12925
|
+
"List the workspace client tags (id, name, color) \u2014 the ids `clients import --tag` accepts"
|
|
12926
|
+
).action(async (_options, command) => {
|
|
12927
|
+
const flags = getGlobalFlags(command);
|
|
12928
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12929
|
+
const result = await client.listClientTags();
|
|
12930
|
+
emit(flags, result, () => {
|
|
12931
|
+
printTable(
|
|
12932
|
+
result.tags.map((tag) => ({ id: tag.id, name: tag.name, color: tag.color ?? "" })),
|
|
12933
|
+
["id", "name", "color"]
|
|
12934
|
+
);
|
|
12935
|
+
});
|
|
12936
|
+
});
|
|
12937
|
+
tags.command("create").description("Create a client tag (names are unique per workspace)").requiredOption("--name <name>", "tag name").option("--color <color>", `tag colour preset (${CLIENT_TAG_COLORS.join(" | ")})`).action(async (options, command) => {
|
|
12938
|
+
const flags = getGlobalFlags(command);
|
|
12939
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12940
|
+
const result = await client.createClientTag({
|
|
12941
|
+
name: options.name,
|
|
12942
|
+
color: options.color !== void 0 ? parseColor(options.color) : void 0
|
|
12943
|
+
});
|
|
12944
|
+
emit(flags, result, () => {
|
|
12945
|
+
process.stdout.write(`Created tag ${result.tag.id} (${result.tag.name}).
|
|
12946
|
+
`);
|
|
12947
|
+
});
|
|
12948
|
+
});
|
|
12949
|
+
tags.command("update").description(
|
|
12950
|
+
"Rename and/or recolour a tag \u2014 the id stays the same, so every assignment, segment and campaign link action keeps pointing at it"
|
|
12951
|
+
).argument("<tagId>", "tag id (from `clients tags list`)").option("--name <name>", "new tag name").option("--color <color>", `new tag colour preset (${CLIENT_TAG_COLORS.join(" | ")})`).option("--clear-color", "remove the colour").action(
|
|
12952
|
+
async (tagId, options, command) => {
|
|
12953
|
+
const flags = getGlobalFlags(command);
|
|
12954
|
+
const input = {};
|
|
12955
|
+
if (options.name !== void 0) input.name = options.name;
|
|
12956
|
+
const color = resolveColor(options);
|
|
12957
|
+
if (color !== void 0) input.color = color;
|
|
12958
|
+
if (Object.keys(input).length === 0) {
|
|
12959
|
+
throw new Error("Nothing to update \u2014 pass --name, --color or --clear-color.");
|
|
12960
|
+
}
|
|
12961
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12962
|
+
const result = await client.updateClientTag(tagId, input);
|
|
12963
|
+
emit(flags, result, () => {
|
|
12964
|
+
process.stdout.write(`Updated tag ${result.tag.id} (${result.tag.name}).
|
|
12965
|
+
`);
|
|
12966
|
+
});
|
|
12967
|
+
}
|
|
12968
|
+
);
|
|
12969
|
+
tags.command("delete").description(
|
|
12970
|
+
"PERMANENTLY delete a tag and remove it from every client that carries it \u2014 cannot be undone. Requires --confirm"
|
|
12971
|
+
).argument("<tagId>", "tag id (from `clients tags list`)").option("--confirm", "required \u2014 acknowledge the permanent deletion").action(async (tagId, options, command) => {
|
|
12972
|
+
const flags = getGlobalFlags(command);
|
|
12973
|
+
if (!options.confirm) {
|
|
12974
|
+
throw new Error(
|
|
12975
|
+
`Refusing to delete tag ${tagId} without --confirm \u2014 it is removed from every client that carries it. Re-run with --confirm to proceed.`
|
|
12976
|
+
);
|
|
12977
|
+
}
|
|
12978
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12979
|
+
const result = await client.deleteClientTag(tagId);
|
|
12980
|
+
emit(flags, result, () => {
|
|
12981
|
+
process.stdout.write(
|
|
12982
|
+
`Deleted tag ${tagId} \u2014 removed from ${result.clientsUntagged} client${result.clientsUntagged === 1 ? "" : "s"}.
|
|
12983
|
+
`
|
|
12984
|
+
);
|
|
12985
|
+
});
|
|
12986
|
+
});
|
|
12987
|
+
tags.command("assign").description(
|
|
12988
|
+
`Add and/or remove tags on MANY clients at once (tag IDS, not names; unknown ids fail the request). Sent in batches of ${MAX_TAG_CLIENTS_PER_REQUEST}`
|
|
12989
|
+
).requiredOption("--id <ids>", "comma-separated workspace client ids (wcid)").option("--add <tagIds>", "comma-separated tag ids to add").option("--remove <tagIds>", "comma-separated tag ids to remove").action(async (options, command) => {
|
|
12990
|
+
const flags = getGlobalFlags(command);
|
|
12991
|
+
const clientIds = parseCommaList2(options.id, "--id");
|
|
12992
|
+
const addTagIds = options.add !== void 0 ? parseCommaList2(options.add, "--add") : [];
|
|
12993
|
+
const removeTagIds = options.remove !== void 0 ? parseCommaList2(options.remove, "--remove") : [];
|
|
12994
|
+
if (addTagIds.length === 0 && removeTagIds.length === 0) {
|
|
12995
|
+
throw new Error("Pass --add and/or --remove with at least one tag id.");
|
|
12996
|
+
}
|
|
12997
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12998
|
+
const summary = { results: [], skippedClientIds: [] };
|
|
12999
|
+
for (let offset = 0; offset < clientIds.length; offset += MAX_TAG_CLIENTS_PER_REQUEST) {
|
|
13000
|
+
const page = await client.bulkUpdateClientTagAssignments({
|
|
13001
|
+
clientIds: clientIds.slice(offset, offset + MAX_TAG_CLIENTS_PER_REQUEST),
|
|
13002
|
+
...addTagIds.length > 0 ? { addTagIds } : {},
|
|
13003
|
+
...removeTagIds.length > 0 ? { removeTagIds } : {}
|
|
13004
|
+
});
|
|
13005
|
+
summary.results.push(...page.results);
|
|
13006
|
+
summary.skippedClientIds.push(...page.skippedClientIds);
|
|
13007
|
+
}
|
|
13008
|
+
emit(flags, summary, () => {
|
|
13009
|
+
process.stdout.write(
|
|
13010
|
+
`Updated tags on ${summary.results.length} client${summary.results.length === 1 ? "" : "s"}${summary.skippedClientIds.length > 0 ? `, skipped ${summary.skippedClientIds.length} (not found / merged)` : ""}.
|
|
13011
|
+
`
|
|
13012
|
+
);
|
|
13013
|
+
});
|
|
13014
|
+
});
|
|
13015
|
+
}
|
|
13016
|
+
|
|
13017
|
+
// src/commands/clients/index.ts
|
|
13018
|
+
var IMPORT_CHUNK_SIZE = 100;
|
|
13019
|
+
var SCHEDULER_UNAVAILABLE_STATUS = 503;
|
|
13020
|
+
var MAX_BULK_FILTERED_PRODUCTS = 10;
|
|
13021
|
+
function requireConfirm4(confirm, what) {
|
|
13022
|
+
if (!confirm) {
|
|
13023
|
+
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
13024
|
+
}
|
|
13025
|
+
}
|
|
13026
|
+
function withSchedulerHint(error) {
|
|
13027
|
+
if (error instanceof ApiError && error.status === SCHEDULER_UNAVAILABLE_STATUS) {
|
|
13028
|
+
throw new ApiError(
|
|
13029
|
+
error.status,
|
|
13030
|
+
"Trigger.dev scheduler not configured \u2014 bulk client actions cannot be started on this environment.",
|
|
13031
|
+
"The API needs TRIGGER_SECRET_KEY (Trigger.dev) set for the workspace services; ask the operator to configure it."
|
|
13032
|
+
);
|
|
13033
|
+
}
|
|
13034
|
+
throw error;
|
|
13035
|
+
}
|
|
13036
|
+
function parseCommaList3(value, flag) {
|
|
13037
|
+
const items = [
|
|
13038
|
+
...new Set(
|
|
13039
|
+
value.split(",").map((item) => item.trim()).filter(Boolean)
|
|
13040
|
+
)
|
|
13041
|
+
];
|
|
13042
|
+
if (items.length === 0) {
|
|
13043
|
+
throw new Error(`Invalid ${flag}: expected at least one value (comma-separated).`);
|
|
13044
|
+
}
|
|
13045
|
+
return items;
|
|
13046
|
+
}
|
|
13047
|
+
function resolveBulkSelection(options) {
|
|
13048
|
+
if (options.email === void 0 === (options.all !== true)) {
|
|
13049
|
+
throw new Error("Pass exactly one of --email <a,b,\u2026> or --all.");
|
|
13050
|
+
}
|
|
13051
|
+
if (options.all) return { processAllClients: true };
|
|
13052
|
+
return { clientEmails: parseCommaList3(options.email ?? "", "--email"), processAllClients: false };
|
|
13053
|
+
}
|
|
13054
|
+
function describeBulkSelection(options) {
|
|
13055
|
+
return options.all ? "ALL active clients" : `client(s) ${options.email ?? ""}`;
|
|
13056
|
+
}
|
|
13057
|
+
function parseExportColumns(value) {
|
|
13058
|
+
return parseCommaList3(value, "--columns").map((column) => {
|
|
13059
|
+
const match = CLIENTS_EXPORT_COLUMN_GROUPS.find((group) => group === column);
|
|
13060
|
+
if (match === void 0) {
|
|
13061
|
+
throw new Error(
|
|
13062
|
+
`Invalid --columns entry: ${column} (expected ${CLIENTS_EXPORT_COLUMN_GROUPS.join(", ")})`
|
|
13063
|
+
);
|
|
13064
|
+
}
|
|
13065
|
+
return match;
|
|
13066
|
+
});
|
|
13067
|
+
}
|
|
13068
|
+
function assertIsoDate(value, flag) {
|
|
13069
|
+
if (Number.isNaN(Date.parse(value))) {
|
|
13070
|
+
throw new Error(`Invalid ${flag}: ${value} (expected an ISO date, e.g. 2026-07-01)`);
|
|
12341
13071
|
}
|
|
12342
13072
|
}
|
|
12343
13073
|
function parseClientsFilter(value) {
|
|
12344
13074
|
const trimmed = value.trim();
|
|
12345
|
-
const raw = trimmed.startsWith("{") ? trimmed :
|
|
13075
|
+
const raw = trimmed.startsWith("{") ? trimmed : readFileSync7(value, "utf8");
|
|
12346
13076
|
let parsed;
|
|
12347
13077
|
try {
|
|
12348
13078
|
parsed = JSON.parse(raw);
|
|
@@ -12386,7 +13116,7 @@ function oneLine(text, max = 80) {
|
|
|
12386
13116
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
12387
13117
|
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
12388
13118
|
}
|
|
12389
|
-
function
|
|
13119
|
+
function parsePositiveInt4(value, flag) {
|
|
12390
13120
|
const n = Number.parseInt(value, 10);
|
|
12391
13121
|
if (!Number.isInteger(n) || n <= 0) {
|
|
12392
13122
|
throw new Error(`Invalid ${flag}: ${value}`);
|
|
@@ -12465,7 +13195,7 @@ function resolveImportColumn(header) {
|
|
|
12465
13195
|
}
|
|
12466
13196
|
}
|
|
12467
13197
|
function readImportCsv(csvFile) {
|
|
12468
|
-
const rows = parseCsv(
|
|
13198
|
+
const rows = parseCsv(readFileSync7(csvFile, "utf8"));
|
|
12469
13199
|
if (rows.length === 0) {
|
|
12470
13200
|
throw new Error(`${csvFile} is empty.`);
|
|
12471
13201
|
}
|
|
@@ -12561,7 +13291,7 @@ function registerClientsCommands(program2) {
|
|
|
12561
13291
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12562
13292
|
const page = await client.listClients({
|
|
12563
13293
|
search: options.search,
|
|
12564
|
-
limit: options.limit !== void 0 ?
|
|
13294
|
+
limit: options.limit !== void 0 ? parsePositiveInt4(options.limit, "--limit") : void 0,
|
|
12565
13295
|
cursor: options.cursor,
|
|
12566
13296
|
products: options.withProducts ? "has" : options.withoutProducts ? "no" : void 0
|
|
12567
13297
|
});
|
|
@@ -12633,25 +13363,13 @@ Next page: --cursor ${page.nextCursor}
|
|
|
12633
13363
|
}
|
|
12634
13364
|
});
|
|
12635
13365
|
});
|
|
12636
|
-
clients2
|
|
12637
|
-
"List the workspace client tags (id, name, color) \u2014 the ids `clients import --tag` accepts"
|
|
12638
|
-
).action(async (_options, command) => {
|
|
12639
|
-
const flags = getGlobalFlags(command);
|
|
12640
|
-
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12641
|
-
const result = await client.listClientTags();
|
|
12642
|
-
emit(flags, result, () => {
|
|
12643
|
-
printTable(
|
|
12644
|
-
result.tags.map((tag) => ({ id: tag.id, name: tag.name, color: tag.color ?? "" })),
|
|
12645
|
-
["id", "name", "color"]
|
|
12646
|
-
);
|
|
12647
|
-
});
|
|
12648
|
-
});
|
|
13366
|
+
registerClientTagsCommands(clients2);
|
|
12649
13367
|
clients2.command("email-log").description("Show the emails Zanfia sent to a client and their delivery status (newest first)").argument("<clientId>", "workspace client id (wcid)").option("--limit <n>", "max entries (default 20, max 50)").action(async (clientId, options, command) => {
|
|
12650
13368
|
const flags = getGlobalFlags(command);
|
|
12651
13369
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12652
13370
|
const result = await client.getClientEmailLog(
|
|
12653
13371
|
clientId,
|
|
12654
|
-
options.limit !== void 0 ?
|
|
13372
|
+
options.limit !== void 0 ? parsePositiveInt4(options.limit, "--limit") : void 0
|
|
12655
13373
|
);
|
|
12656
13374
|
emit(flags, result, () => {
|
|
12657
13375
|
process.stdout.write(`Email log for ${result.email}
|
|
@@ -12669,6 +13387,26 @@ Next page: --cursor ${page.nextCursor}
|
|
|
12669
13387
|
);
|
|
12670
13388
|
});
|
|
12671
13389
|
});
|
|
13390
|
+
clients2.command("bundled-access").description(
|
|
13391
|
+
"Clients whose access to a product comes from a PARENT product's plan (bundle), not a direct grant \u2014 together with the direct grants this is the full access census of the product"
|
|
13392
|
+
).argument("<productId>", "the INCLUDED product id").action(async (productId, _options, command) => {
|
|
13393
|
+
const flags = getGlobalFlags(command);
|
|
13394
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
13395
|
+
const result = await client.listBundledAccessClients(productId);
|
|
13396
|
+
emit(flags, result, () => {
|
|
13397
|
+
printTable(
|
|
13398
|
+
result.items.map((item) => ({
|
|
13399
|
+
clientId: item.clientId,
|
|
13400
|
+
email: item.email,
|
|
13401
|
+
name: [item.firstName, item.lastName].filter(Boolean).join(" "),
|
|
13402
|
+
via: item.viaProductName,
|
|
13403
|
+
plan: item.viaPlanName,
|
|
13404
|
+
since: item.sinceAt ?? ""
|
|
13405
|
+
})),
|
|
13406
|
+
["clientId", "email", "name", "via", "plan", "since"]
|
|
13407
|
+
);
|
|
13408
|
+
});
|
|
13409
|
+
});
|
|
12672
13410
|
clients2.command("accesses").description("List a client's product-access grants (active + archived), newest first").argument("<clientId>", "workspace client id (wcid)").option("--status <status>", `only grants in this state (${ACCESS_STATUSES.join(" | ")})`).action(async (clientId, options, command) => {
|
|
12673
13411
|
const flags = getGlobalFlags(command);
|
|
12674
13412
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
@@ -12881,14 +13619,14 @@ Next page: --cursor ${page.nextCursor}
|
|
|
12881
13619
|
let existing = 0;
|
|
12882
13620
|
let invalid = 0;
|
|
12883
13621
|
for (let offset = 0; offset < contacts.length; offset += IMPORT_CHUNK_SIZE) {
|
|
12884
|
-
const
|
|
13622
|
+
const chunk2 = contacts.slice(offset, offset + IMPORT_CHUNK_SIZE);
|
|
12885
13623
|
if (!flags.json && contacts.length > IMPORT_CHUNK_SIZE) {
|
|
12886
13624
|
process.stderr.write(
|
|
12887
|
-
`Importing rows ${offset + 1}\u2013${offset +
|
|
13625
|
+
`Importing rows ${offset + 1}\u2013${offset + chunk2.length} of ${contacts.length}\u2026
|
|
12888
13626
|
`
|
|
12889
13627
|
);
|
|
12890
13628
|
}
|
|
12891
|
-
const page = await client.importClients({ contacts:
|
|
13629
|
+
const page = await client.importClients({ contacts: chunk2, tagIds });
|
|
12892
13630
|
results.push(...page.results);
|
|
12893
13631
|
created += page.created;
|
|
12894
13632
|
existing += page.existing;
|
|
@@ -12946,7 +13684,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
12946
13684
|
async (productId, options, command) => {
|
|
12947
13685
|
const flags = getGlobalFlags(command);
|
|
12948
13686
|
const selection = resolveBulkSelection(options);
|
|
12949
|
-
|
|
13687
|
+
requireConfirm4(
|
|
12950
13688
|
options.confirm,
|
|
12951
13689
|
`revoke ${describeBulkSelection(options)} on product ${productId} (no refunds)`
|
|
12952
13690
|
);
|
|
@@ -12983,7 +13721,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
12983
13721
|
}
|
|
12984
13722
|
addDaysToCurrentEndAccessPeriod = days;
|
|
12985
13723
|
}
|
|
12986
|
-
|
|
13724
|
+
requireConfirm4(
|
|
12987
13725
|
options.confirm,
|
|
12988
13726
|
`change the access period of ${describeBulkSelection(options)} on product ${productId}`
|
|
12989
13727
|
);
|
|
@@ -13004,7 +13742,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
13004
13742
|
async (productId, options, command) => {
|
|
13005
13743
|
const flags = getGlobalFlags(command);
|
|
13006
13744
|
const selection = resolveBulkSelection(options);
|
|
13007
|
-
|
|
13745
|
+
requireConfirm4(
|
|
13008
13746
|
options.confirm,
|
|
13009
13747
|
`delete the grants of ${describeBulkSelection(options)} on product ${productId}`
|
|
13010
13748
|
);
|
|
@@ -13027,14 +13765,14 @@ Next page: --cursor ${page.nextCursor}
|
|
|
13027
13765
|
).option("--run-tasks", "also run each product's access-end automation tasks per client").option("--confirm", "required \u2014 acknowledge the filter-wide revoke").action(
|
|
13028
13766
|
async (options, command) => {
|
|
13029
13767
|
const flags = getGlobalFlags(command);
|
|
13030
|
-
const productIds =
|
|
13768
|
+
const productIds = parseCommaList3(options.product, "--product");
|
|
13031
13769
|
if (productIds.length > MAX_BULK_FILTERED_PRODUCTS) {
|
|
13032
13770
|
throw new Error(
|
|
13033
13771
|
`--product accepts at most ${MAX_BULK_FILTERED_PRODUCTS} ids (got ${productIds.length}).`
|
|
13034
13772
|
);
|
|
13035
13773
|
}
|
|
13036
13774
|
const clientsFilter = parseClientsFilter(options.filter);
|
|
13037
|
-
|
|
13775
|
+
requireConfirm4(
|
|
13038
13776
|
options.confirm,
|
|
13039
13777
|
`revoke every client matching the filter in ${productIds.length} product(s)`
|
|
13040
13778
|
);
|
|
@@ -13054,14 +13792,14 @@ Next page: --cursor ${page.nextCursor}
|
|
|
13054
13792
|
`\u26A0\uFE0F Delete (tombstone) clients by id \u2014 clients that still hold active product access are SKIPPED (revoke first). Sent in batches of ${MAX_DELETE_CLIENTS_PER_REQUEST}. Requires --confirm`
|
|
13055
13793
|
).requiredOption("--id <ids>", "comma-separated workspace client ids (wcid)").option("--confirm", "required \u2014 acknowledge the deletion").action(async (options, command) => {
|
|
13056
13794
|
const flags = getGlobalFlags(command);
|
|
13057
|
-
const clientIds =
|
|
13058
|
-
|
|
13795
|
+
const clientIds = parseCommaList3(options.id, "--id");
|
|
13796
|
+
requireConfirm4(options.confirm, `delete ${clientIds.length} client(s)`);
|
|
13059
13797
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
13060
13798
|
const deletedClientIds = [];
|
|
13061
13799
|
const skipped = [];
|
|
13062
13800
|
for (let offset = 0; offset < clientIds.length; offset += MAX_DELETE_CLIENTS_PER_REQUEST) {
|
|
13063
|
-
const
|
|
13064
|
-
const page = await client.deleteClients({ clientIds:
|
|
13801
|
+
const chunk2 = clientIds.slice(offset, offset + MAX_DELETE_CLIENTS_PER_REQUEST);
|
|
13802
|
+
const page = await client.deleteClients({ clientIds: chunk2 });
|
|
13065
13803
|
deletedClientIds.push(...page.deletedClientIds);
|
|
13066
13804
|
skipped.push(...page.skipped);
|
|
13067
13805
|
}
|
|
@@ -13102,7 +13840,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
13102
13840
|
columns: options.columns !== void 0 ? parseExportColumns(options.columns) : void 0,
|
|
13103
13841
|
createdFrom: options.createdFrom,
|
|
13104
13842
|
createdTo: options.createdTo,
|
|
13105
|
-
limit: options.limit !== void 0 ?
|
|
13843
|
+
limit: options.limit !== void 0 ? parsePositiveInt4(options.limit, "--limit") : void 0
|
|
13106
13844
|
});
|
|
13107
13845
|
writeFileSync2(options.out, result.csv, "utf8");
|
|
13108
13846
|
emit(
|
|
@@ -13149,7 +13887,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
13149
13887
|
).argument("<clientId>", "client id").argument("<deviceId>", "device id from `clients devices <clientId>`").option("--confirm", "required \u2014 acknowledge the device sign-out").action(
|
|
13150
13888
|
async (clientId, deviceId, options, command) => {
|
|
13151
13889
|
const flags = getGlobalFlags(command);
|
|
13152
|
-
|
|
13890
|
+
requireConfirm4(options.confirm, `revoke device ${deviceId} of client ${clientId}`);
|
|
13153
13891
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
13154
13892
|
const result = await client.revokeClientDevice(clientId, deviceId);
|
|
13155
13893
|
emit(flags, result, () => {
|
|
@@ -13165,7 +13903,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
13165
13903
|
"Revoke EVERY device slot of the client (all devices signed out, limit counters cleared). Requires --confirm"
|
|
13166
13904
|
).argument("<clientId>", "client id").option("--confirm", "required \u2014 acknowledge signing out every device").action(async (clientId, options, command) => {
|
|
13167
13905
|
const flags = getGlobalFlags(command);
|
|
13168
|
-
|
|
13906
|
+
requireConfirm4(options.confirm, `reset every device of client ${clientId}`);
|
|
13169
13907
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
13170
13908
|
const result = await client.resetClientDevices(clientId);
|
|
13171
13909
|
emit(flags, result, () => {
|
|
@@ -13173,14 +13911,16 @@ Next page: --cursor ${page.nextCursor}
|
|
|
13173
13911
|
`);
|
|
13174
13912
|
});
|
|
13175
13913
|
});
|
|
13914
|
+
registerClientSubscriptionCommands(clients2);
|
|
13915
|
+
registerClientEmailsCommands(clients2);
|
|
13176
13916
|
}
|
|
13177
13917
|
|
|
13178
13918
|
// src/commands/communities/index.ts
|
|
13179
|
-
import { readFileSync as
|
|
13919
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
13180
13920
|
function clientFor(flags) {
|
|
13181
13921
|
return createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
13182
13922
|
}
|
|
13183
|
-
function
|
|
13923
|
+
function requireConfirm5(confirm, what) {
|
|
13184
13924
|
if (!confirm) {
|
|
13185
13925
|
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
13186
13926
|
}
|
|
@@ -13192,7 +13932,7 @@ function parseOnOff(value, flag) {
|
|
|
13192
13932
|
}
|
|
13193
13933
|
return normalized === "on";
|
|
13194
13934
|
}
|
|
13195
|
-
function
|
|
13935
|
+
function parsePositiveInt5(value, flag) {
|
|
13196
13936
|
const n = Number.parseInt(value, 10);
|
|
13197
13937
|
if (!Number.isInteger(n) || n <= 0) {
|
|
13198
13938
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer).`);
|
|
@@ -13282,11 +14022,11 @@ function authorLabel(author) {
|
|
|
13282
14022
|
const name = [author.firstName, author.lastName].filter(Boolean).join(" ");
|
|
13283
14023
|
return author.nickname || name || author.id;
|
|
13284
14024
|
}
|
|
13285
|
-
function
|
|
14025
|
+
function resolveContent3(options) {
|
|
13286
14026
|
if (options.content !== void 0 && options.contentFile !== void 0) {
|
|
13287
14027
|
throw new Error("Use either --content or --content-file, not both.");
|
|
13288
14028
|
}
|
|
13289
|
-
return options.contentFile !== void 0 ?
|
|
14029
|
+
return options.contentFile !== void 0 ? readFileSync8(options.contentFile, "utf8") : options.content;
|
|
13290
14030
|
}
|
|
13291
14031
|
function parsePage(options) {
|
|
13292
14032
|
return {
|
|
@@ -13430,7 +14170,7 @@ Read a thread: --thread <messageId>
|
|
|
13430
14170
|
chat.command("send").description("Send a message to a chat channel (use --reply-to for a thread reply)").argument("<communityId>", "community id").argument("<channelId>", "chat channel id").option("--content <text>", "message body").option("--content-file <path>", "read the message body from a file").option("--reply-to <messageId>", "reply in this message's thread").action(
|
|
13431
14171
|
async (communityId, channelId, options, command) => {
|
|
13432
14172
|
const flags = getGlobalFlags(command);
|
|
13433
|
-
const content =
|
|
14173
|
+
const content = resolveContent3(options);
|
|
13434
14174
|
if (content === void 0 || content.trim().length === 0) {
|
|
13435
14175
|
throw new Error("Pass the message body via --content or --content-file.");
|
|
13436
14176
|
}
|
|
@@ -13450,7 +14190,7 @@ Read a thread: --thread <messageId>
|
|
|
13450
14190
|
chat.command("edit").description("Edit an own chat message (pass --thread when it is a thread reply)").argument("<communityId>", "community id").argument("<channelId>", "chat channel id").argument("<messageId>", "message id").option("--content <text>", "new message body").option("--content-file <path>", "read the new message body from a file").option("--thread <parentMessageId>", "the message is a reply in this thread").action(
|
|
13451
14191
|
async (communityId, channelId, messageId, options, command) => {
|
|
13452
14192
|
const flags = getGlobalFlags(command);
|
|
13453
|
-
const content =
|
|
14193
|
+
const content = resolveContent3(options);
|
|
13454
14194
|
if (content === void 0 || content.trim().length === 0) {
|
|
13455
14195
|
throw new Error("Pass the new message body via --content or --content-file.");
|
|
13456
14196
|
}
|
|
@@ -13670,7 +14410,7 @@ Read a thread: --thread <messageId>
|
|
|
13670
14410
|
post.command("create").description("Create a post in a feed channel").argument("<communityId>", "community id").argument("<channelId>", "channel id").option("--title <title>", "post title").option("--content <markdown>", "post body (Markdown)").option("--content-file <path>", "read the post body (Markdown) from a file").action(
|
|
13671
14411
|
async (communityId, channelId, options, command) => {
|
|
13672
14412
|
const flags = getGlobalFlags(command);
|
|
13673
|
-
const content =
|
|
14413
|
+
const content = resolveContent3(options);
|
|
13674
14414
|
if (content === void 0 || content.trim().length === 0) {
|
|
13675
14415
|
throw new Error("Pass the post body via --content or --content-file.");
|
|
13676
14416
|
}
|
|
@@ -13687,7 +14427,7 @@ Read a thread: --thread <messageId>
|
|
|
13687
14427
|
post.command("update").description("Update a post (title and/or Markdown content)").argument("<communityId>", "community id").argument("<postId>", "post id").option("--title <title>", "new post title").option("--content <markdown>", "new post body (Markdown)").option("--content-file <path>", "read the new post body (Markdown) from a file").action(
|
|
13688
14428
|
async (communityId, postId, options, command) => {
|
|
13689
14429
|
const flags = getGlobalFlags(command);
|
|
13690
|
-
const content =
|
|
14430
|
+
const content = resolveContent3(options);
|
|
13691
14431
|
if (options.title === void 0 && content === void 0) {
|
|
13692
14432
|
throw new Error("Nothing to update \u2014 pass --title and/or --content/--content-file.");
|
|
13693
14433
|
}
|
|
@@ -13756,7 +14496,7 @@ Next page: --cursor ${result.nextCursor}
|
|
|
13756
14496
|
comment.command("create").description("Comment on a post (use --reply-to for a threaded reply)").argument("<communityId>", "community id").argument("<postId>", "post id").option("--content <markdown>", "comment body (Markdown)").option("--content-file <path>", "read the comment body (Markdown) from a file").option("--reply-to <commentId>", "parent comment id for a threaded reply").action(
|
|
13757
14497
|
async (communityId, postId, options, command) => {
|
|
13758
14498
|
const flags = getGlobalFlags(command);
|
|
13759
|
-
const content =
|
|
14499
|
+
const content = resolveContent3(options);
|
|
13760
14500
|
if (content === void 0 || content.trim().length === 0) {
|
|
13761
14501
|
throw new Error("Pass the comment body via --content or --content-file.");
|
|
13762
14502
|
}
|
|
@@ -13773,7 +14513,7 @@ Next page: --cursor ${result.nextCursor}
|
|
|
13773
14513
|
comment.command("update").description("Update a comment (Markdown content)").argument("<communityId>", "community id").argument("<commentId>", "comment id").option("--content <markdown>", "new comment body (Markdown)").option("--content-file <path>", "read the new comment body (Markdown) from a file").action(
|
|
13774
14514
|
async (communityId, commentId, options, command) => {
|
|
13775
14515
|
const flags = getGlobalFlags(command);
|
|
13776
|
-
const content =
|
|
14516
|
+
const content = resolveContent3(options);
|
|
13777
14517
|
if (content === void 0 || content.trim().length === 0) {
|
|
13778
14518
|
throw new Error("Pass the new comment body via --content or --content-file.");
|
|
13779
14519
|
}
|
|
@@ -13861,7 +14601,7 @@ ${message.content}
|
|
|
13861
14601
|
support.command("create").description("Raise a support ticket in a community (needs a support channel)").argument("<communityId>", "community id").requiredOption("--channel <channelId>", "the support channel id").requiredOption("--subject <subject>", "ticket subject").option("--message <markdown>", "ticket body (Markdown)").option("--message-file <path>", "read the ticket body (Markdown) from a file").action(
|
|
13862
14602
|
async (communityId, options, command) => {
|
|
13863
14603
|
const flags = getGlobalFlags(command);
|
|
13864
|
-
const message =
|
|
14604
|
+
const message = resolveContent3({
|
|
13865
14605
|
content: options.message,
|
|
13866
14606
|
contentFile: options.messageFile
|
|
13867
14607
|
});
|
|
@@ -13882,7 +14622,7 @@ ${message.content}
|
|
|
13882
14622
|
support.command("reply").description("Reply on a support ticket").argument("<communityId>", "community id").argument("<ticketId>", "ticket id").option("--content <markdown>", "reply body (Markdown)").option("--content-file <path>", "read the reply body (Markdown) from a file").action(
|
|
13883
14623
|
async (communityId, ticketId, options, command) => {
|
|
13884
14624
|
const flags = getGlobalFlags(command);
|
|
13885
|
-
const content =
|
|
14625
|
+
const content = resolveContent3(options);
|
|
13886
14626
|
if (content === void 0 || content.trim().length === 0) {
|
|
13887
14627
|
throw new Error("Pass the reply body via --content or --content-file.");
|
|
13888
14628
|
}
|
|
@@ -14056,7 +14796,7 @@ Next page: --cursor ${result.nextCursor}
|
|
|
14056
14796
|
const raw = options.autoHideAfterReports.trim().toLowerCase();
|
|
14057
14797
|
patch.moderation = {
|
|
14058
14798
|
...patch.moderation ?? current.moderation ?? {},
|
|
14059
|
-
autoHideAfterReports: raw === "off" ? null :
|
|
14799
|
+
autoHideAfterReports: raw === "off" ? null : parsePositiveInt5(options.autoHideAfterReports, "--auto-hide-after-reports")
|
|
14060
14800
|
};
|
|
14061
14801
|
}
|
|
14062
14802
|
const keys = Object.keys(patch);
|
|
@@ -14194,7 +14934,7 @@ Next page: --cursor ${result.nextCursor}
|
|
|
14194
14934
|
from: options.from !== void 0 ? assertIsoInstant(options.from, "--from") : void 0,
|
|
14195
14935
|
to: options.to !== void 0 ? assertIsoInstant(options.to, "--to") : void 0,
|
|
14196
14936
|
excludeCancelled: options.excludeCancelled,
|
|
14197
|
-
limit: options.limit !== void 0 ?
|
|
14937
|
+
limit: options.limit !== void 0 ? parsePositiveInt5(options.limit, "--limit") : void 0
|
|
14198
14938
|
});
|
|
14199
14939
|
emit(flags, result, () => {
|
|
14200
14940
|
printTable(result.events.map(eventRow), EVENT_COLUMNS);
|
|
@@ -14282,7 +15022,7 @@ Next page: --cursor ${result.nextCursor}
|
|
|
14282
15022
|
events.command("delete").description("Delete a calendar event (staff) \u2014 cannot be undone. Requires --confirm.").argument("<communityId>", "community id").argument("<eventId>", "event id").option("--confirm", "required \u2014 acknowledge the deletion").action(
|
|
14283
15023
|
async (communityId, eventId, options, command) => {
|
|
14284
15024
|
const flags = getGlobalFlags(command);
|
|
14285
|
-
|
|
15025
|
+
requireConfirm5(options.confirm, `delete event ${eventId}`);
|
|
14286
15026
|
await clientFor(flags).deleteCommunityEvent(communityId, eventId);
|
|
14287
15027
|
emit(flags, { communityId, eventId, deleted: true }, () => {
|
|
14288
15028
|
process.stdout.write(`Deleted event ${eventId} from community ${communityId}.
|
|
@@ -14316,7 +15056,7 @@ Recently resolved (${result.resolved.length}):
|
|
|
14316
15056
|
COMMUNITY_MODERATION_RESOLVE_ACTIONS,
|
|
14317
15057
|
"--action"
|
|
14318
15058
|
);
|
|
14319
|
-
|
|
15059
|
+
requireConfirm5(options.confirm, `${action} the content behind report ${reportId}`);
|
|
14320
15060
|
await clientFor(flags).resolveCommunityModerationReport(communityId, reportId, {
|
|
14321
15061
|
action,
|
|
14322
15062
|
...options.note !== void 0 ? { note: options.note } : {}
|
|
@@ -14363,7 +15103,7 @@ function parseSettingsPatch(value) {
|
|
|
14363
15103
|
}
|
|
14364
15104
|
function parseJsonObject(value, flag) {
|
|
14365
15105
|
const trimmed = value.trim();
|
|
14366
|
-
const raw = trimmed.startsWith("{") ? trimmed :
|
|
15106
|
+
const raw = trimmed.startsWith("{") ? trimmed : readFileSync8(value, "utf8");
|
|
14367
15107
|
let parsed;
|
|
14368
15108
|
try {
|
|
14369
15109
|
parsed = JSON.parse(raw);
|
|
@@ -14377,20 +15117,20 @@ function parseJsonObject(value, flag) {
|
|
|
14377
15117
|
}
|
|
14378
15118
|
|
|
14379
15119
|
// src/commands/conversations/index.ts
|
|
14380
|
-
import { readFileSync as
|
|
15120
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
14381
15121
|
var MAX_BROADCAST_RECIPIENTS = 5e3;
|
|
14382
|
-
function
|
|
15122
|
+
function parsePositiveInt6(value, flag) {
|
|
14383
15123
|
const n = Number.parseInt(value, 10);
|
|
14384
15124
|
if (!Number.isInteger(n) || n <= 0) {
|
|
14385
15125
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
14386
15126
|
}
|
|
14387
15127
|
return n;
|
|
14388
15128
|
}
|
|
14389
|
-
function
|
|
15129
|
+
function resolveContent4(options) {
|
|
14390
15130
|
if (options.content !== void 0 && options.contentFile !== void 0) {
|
|
14391
15131
|
throw new Error("Use either --content or --content-file, not both.");
|
|
14392
15132
|
}
|
|
14393
|
-
const content = options.contentFile !== void 0 ?
|
|
15133
|
+
const content = options.contentFile !== void 0 ? readFileSync9(options.contentFile, "utf8") : options.content;
|
|
14394
15134
|
if (content === void 0 || content.trim().length === 0) {
|
|
14395
15135
|
throw new Error("Pass the message body via --content or --content-file.");
|
|
14396
15136
|
}
|
|
@@ -14425,7 +15165,7 @@ function registerConversationsCommands(program2) {
|
|
|
14425
15165
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
14426
15166
|
const page = await client.listConversations({
|
|
14427
15167
|
unreadOnly: options.unread,
|
|
14428
|
-
limit: options.limit !== void 0 ?
|
|
15168
|
+
limit: options.limit !== void 0 ? parsePositiveInt6(options.limit, "--limit") : void 0,
|
|
14429
15169
|
cursor: options.cursor
|
|
14430
15170
|
});
|
|
14431
15171
|
emit(flags, page, () => {
|
|
@@ -14475,7 +15215,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
14475
15215
|
const flags = getGlobalFlags(command);
|
|
14476
15216
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
14477
15217
|
const page = await client.listConversationMessages(clientId, {
|
|
14478
|
-
limit: options.limit !== void 0 ?
|
|
15218
|
+
limit: options.limit !== void 0 ? parsePositiveInt6(options.limit, "--limit") : void 0,
|
|
14479
15219
|
cursor: options.cursor
|
|
14480
15220
|
});
|
|
14481
15221
|
emit(flags, page, () => {
|
|
@@ -14500,7 +15240,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
14500
15240
|
conversations.command("send").description("Send a staff message into a client's conversation").argument("<clientId>", "client id (wcid)").option("--content <text>", "message body").option("--content-file <path>", "read the message body from a file").option("--message-id <id>", "idempotency id \u2014 resending with the same id is a no-op").action(
|
|
14501
15241
|
async (clientId, options, command) => {
|
|
14502
15242
|
const flags = getGlobalFlags(command);
|
|
14503
|
-
const content =
|
|
15243
|
+
const content = resolveContent4(options);
|
|
14504
15244
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
14505
15245
|
const result = await client.sendConversationMessage(clientId, {
|
|
14506
15246
|
content,
|
|
@@ -14515,7 +15255,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
14515
15255
|
conversations.command("edit").description("Edit an own message (author-only)").argument("<clientId>", "client id (wcid)").argument("<messageId>", "message id").option("--content <text>", "new message body").option("--content-file <path>", "read the new message body from a file").action(
|
|
14516
15256
|
async (clientId, messageId, options, command) => {
|
|
14517
15257
|
const flags = getGlobalFlags(command);
|
|
14518
|
-
const content =
|
|
15258
|
+
const content = resolveContent4(options);
|
|
14519
15259
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
14520
15260
|
const result = await client.editConversationMessage(clientId, messageId, content);
|
|
14521
15261
|
emit(flags, result, () => {
|
|
@@ -14575,7 +15315,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
14575
15315
|
const flags = getGlobalFlags(command);
|
|
14576
15316
|
const recipients = [
|
|
14577
15317
|
...options.recipients ?? [],
|
|
14578
|
-
...options.recipientsFile !== void 0 ?
|
|
15318
|
+
...options.recipientsFile !== void 0 ? readFileSync9(options.recipientsFile, "utf8").split(/[\r\n,]+/).map((id) => id.trim()).filter((id) => id.length > 0) : []
|
|
14579
15319
|
];
|
|
14580
15320
|
if (recipients.length === 0) {
|
|
14581
15321
|
throw new Error("Pass recipients via --recipients and/or --recipients-file.");
|
|
@@ -14585,7 +15325,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
14585
15325
|
`Too many recipients: ${recipients.length} (max ${MAX_BROADCAST_RECIPIENTS} per broadcast).`
|
|
14586
15326
|
);
|
|
14587
15327
|
}
|
|
14588
|
-
const content =
|
|
15328
|
+
const content = resolveContent4(options);
|
|
14589
15329
|
if (!options.confirm) {
|
|
14590
15330
|
throw new Error(
|
|
14591
15331
|
`Refusing to broadcast without --confirm. This sends a REAL DM to all ${recipients.length} listed client(s) and cannot be recalled once fanned out. Re-run with --confirm to proceed.`
|
|
@@ -14621,7 +15361,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
14621
15361
|
}
|
|
14622
15362
|
|
|
14623
15363
|
// src/commands/courses/index.ts
|
|
14624
|
-
import { readFileSync as
|
|
15364
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
|
|
14625
15365
|
var LESSON_STATUSES = ["draft", "public", "hidden", "locked"];
|
|
14626
15366
|
var LESSON_TYPES = ["article", "video", "externalVideo", "task", "quiz"];
|
|
14627
15367
|
var LEARNERS_SORTS = [
|
|
@@ -14738,7 +15478,7 @@ function parseLearnersSort(value) {
|
|
|
14738
15478
|
}
|
|
14739
15479
|
return match;
|
|
14740
15480
|
}
|
|
14741
|
-
function
|
|
15481
|
+
function parsePositiveInt7(value, flag) {
|
|
14742
15482
|
const n = Number.parseInt(value, 10);
|
|
14743
15483
|
if (!Number.isInteger(n) || n <= 0) {
|
|
14744
15484
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
@@ -14797,7 +15537,7 @@ function parseBool(value, flag) {
|
|
|
14797
15537
|
}
|
|
14798
15538
|
function parseJsonFileOrInline2(value, flag) {
|
|
14799
15539
|
const trimmed = value.trim();
|
|
14800
|
-
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed :
|
|
15540
|
+
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync10(value, "utf8");
|
|
14801
15541
|
try {
|
|
14802
15542
|
return JSON.parse(raw);
|
|
14803
15543
|
} catch (error) {
|
|
@@ -14890,7 +15630,7 @@ function registerCoursesCommands(program2) {
|
|
|
14890
15630
|
const report = await notACourseGuard(
|
|
14891
15631
|
productId,
|
|
14892
15632
|
() => clientFor2(flags).getCourseAnalytics(productId, {
|
|
14893
|
-
learnersLimit: options.learnersLimit !== void 0 ?
|
|
15633
|
+
learnersLimit: options.learnersLimit !== void 0 ? parsePositiveInt7(options.learnersLimit, "--learners-limit") : void 0,
|
|
14894
15634
|
learnersSort: options.learnersSort !== void 0 ? parseLearnersSort(options.learnersSort) : void 0
|
|
14895
15635
|
})
|
|
14896
15636
|
);
|
|
@@ -15155,6 +15895,33 @@ Learners (${l.returned} of ${l.total}; progress basis = ${l.lessonsBasis} lesson
|
|
|
15155
15895
|
});
|
|
15156
15896
|
}
|
|
15157
15897
|
);
|
|
15898
|
+
lesson.command("set-attachments").description(
|
|
15899
|
+
"Replace the lesson attachments (downloadable files) with media-library items, in the given order \u2014 pass the FULL list; `lesson get` shows the current uids"
|
|
15900
|
+
).argument("<productId>", "course product id").argument("<lessonId>", "lesson id").option(
|
|
15901
|
+
"--media <mediaIds>",
|
|
15902
|
+
"comma-separated media-library ids (from `media upload` / `media list`), display order"
|
|
15903
|
+
).option("--clear", "remove all attachments").action(
|
|
15904
|
+
async (productId, lessonId, options, command) => {
|
|
15905
|
+
const flags = getGlobalFlags(command);
|
|
15906
|
+
if (options.media === void 0 === !options.clear) {
|
|
15907
|
+
throw new Error("Pass exactly one of --media <id,id,...> or --clear.");
|
|
15908
|
+
}
|
|
15909
|
+
const attachmentMediaIds = options.clear ? [] : (options.media ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
15910
|
+
if (!options.clear && attachmentMediaIds.length === 0) {
|
|
15911
|
+
throw new Error("--media needs at least one media id (or use --clear).");
|
|
15912
|
+
}
|
|
15913
|
+
const result = await clientFor2(flags).updateCourseLesson(productId, lessonId, {
|
|
15914
|
+
attachmentMediaIds
|
|
15915
|
+
});
|
|
15916
|
+
emit(flags, result, () => {
|
|
15917
|
+
process.stdout.write(
|
|
15918
|
+
attachmentMediaIds.length === 0 ? `Cleared attachments of lesson ${result.lessonId}
|
|
15919
|
+
` : `Set ${attachmentMediaIds.length} attachment(s) on lesson ${result.lessonId}
|
|
15920
|
+
`
|
|
15921
|
+
);
|
|
15922
|
+
});
|
|
15923
|
+
}
|
|
15924
|
+
);
|
|
15158
15925
|
lesson.command("set-thumbnail").description(
|
|
15159
15926
|
"Set or clear the lesson thumbnail; Bunny video lessons also get it as the player thumbnail"
|
|
15160
15927
|
).argument("<productId>", "course product id").argument("<lessonId>", "lesson id").option("--media <mediaId>", "media-library IMAGE id (from `media upload`)").option("--url <httpsUrl>", "already-hosted https image URL").option("--clear", "remove the thumbnail (video lessons fall back to the generated frame)").action(
|
|
@@ -15297,7 +16064,7 @@ ${NOT_SAVED_NOTE} --moments=${options.out ?? "<file>"} --confirm\`.
|
|
|
15297
16064
|
if (options.content !== void 0 && options.contentFile !== void 0) {
|
|
15298
16065
|
throw new Error("Use either --content or --content-file, not both.");
|
|
15299
16066
|
}
|
|
15300
|
-
const content = options.contentFile !== void 0 ?
|
|
16067
|
+
const content = options.contentFile !== void 0 ? readFileSync10(options.contentFile, "utf8") : options.content;
|
|
15301
16068
|
if (options.title === void 0 && options.description === void 0 && options.status === void 0 && content === void 0) {
|
|
15302
16069
|
throw new Error(
|
|
15303
16070
|
"Nothing to update \u2014 pass at least one of --title, --description, --status, --content/--content-file."
|
|
@@ -15540,7 +16307,7 @@ ${NOT_SAVED_NOTE} --moments=${options.out ?? "<file>"} --confirm\`.
|
|
|
15540
16307
|
}
|
|
15541
16308
|
|
|
15542
16309
|
// src/commands/discounts/index.ts
|
|
15543
|
-
function
|
|
16310
|
+
function parsePositiveInt8(value, flag) {
|
|
15544
16311
|
const n = Number.parseInt(value, 10);
|
|
15545
16312
|
if (!Number.isInteger(n) || n <= 0) {
|
|
15546
16313
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
@@ -15600,8 +16367,8 @@ function registerDiscountsCommands(program2) {
|
|
|
15600
16367
|
throw new Error("--currency only applies with --amount-off.");
|
|
15601
16368
|
}
|
|
15602
16369
|
const percentOff = options.percentOff !== void 0 ? parsePercent(options.percentOff, "--percent-off") : void 0;
|
|
15603
|
-
const amountOffCents = options.amountOff !== void 0 ?
|
|
15604
|
-
const maxRedemptions = options.maxRedemptions !== void 0 ?
|
|
16370
|
+
const amountOffCents = options.amountOff !== void 0 ? parsePositiveInt8(options.amountOff, "--amount-off") : void 0;
|
|
16371
|
+
const maxRedemptions = options.maxRedemptions !== void 0 ? parsePositiveInt8(options.maxRedemptions, "--max-redemptions") : void 0;
|
|
15605
16372
|
const endsAt = options.ends !== void 0 ? parseIsoDate(options.ends, "--ends") : void 0;
|
|
15606
16373
|
const startsAt = options.starts !== void 0 ? parseIsoDate(options.starts, "--starts") : void 0;
|
|
15607
16374
|
const duration = options.forever ? "forever" : void 0;
|
|
@@ -15657,12 +16424,12 @@ function registerDiscountsCommands(program2) {
|
|
|
15657
16424
|
input.percentOff = parsePercent(options.percentOff, "--percent-off");
|
|
15658
16425
|
}
|
|
15659
16426
|
if (options.amountOff !== void 0) {
|
|
15660
|
-
input.amountOffCents =
|
|
16427
|
+
input.amountOffCents = parsePositiveInt8(options.amountOff, "--amount-off");
|
|
15661
16428
|
}
|
|
15662
16429
|
if (options.currency !== void 0) input.currency = options.currency;
|
|
15663
16430
|
if (options.product.length > 0) input.productIds = options.product;
|
|
15664
16431
|
if (options.maxRedemptions !== void 0) {
|
|
15665
|
-
input.maxRedemptions =
|
|
16432
|
+
input.maxRedemptions = parsePositiveInt8(options.maxRedemptions, "--max-redemptions");
|
|
15666
16433
|
}
|
|
15667
16434
|
if (options.clearMaxRedemptions) input.maxRedemptions = null;
|
|
15668
16435
|
if (options.ends !== void 0) input.endsAt = parseIsoDate(options.ends, "--ends");
|
|
@@ -15722,45 +16489,268 @@ function formatDiscountValue(percentOff, amountOffCents, currency) {
|
|
|
15722
16489
|
}
|
|
15723
16490
|
|
|
15724
16491
|
// src/commands/emails/index.ts
|
|
15725
|
-
import { readFileSync as
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15729
|
-
|
|
16492
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
16493
|
+
|
|
16494
|
+
// src/commands/emails/library.ts
|
|
16495
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
16496
|
+
function resolveContent5(options) {
|
|
16497
|
+
if (options.content !== void 0 && options.contentFile !== void 0) {
|
|
16498
|
+
throw new Error("Use either --content or --content-file, not both.");
|
|
16499
|
+
}
|
|
16500
|
+
return options.contentFile !== void 0 ? readFileSync11(options.contentFile, "utf8") : options.content;
|
|
15730
16501
|
}
|
|
15731
|
-
function
|
|
15732
|
-
if (
|
|
15733
|
-
|
|
15734
|
-
if (!EMAIL_LANGUAGES.includes(normalized)) {
|
|
15735
|
-
throw new Error(`Invalid --lang: ${value} (expected ${EMAIL_LANGUAGES.join(" | ")})`);
|
|
16502
|
+
function resolveBranded2(options) {
|
|
16503
|
+
if (options.branded && options.plain) {
|
|
16504
|
+
throw new Error("Use either --branded or --plain, not both.");
|
|
15736
16505
|
}
|
|
15737
|
-
return
|
|
16506
|
+
if (options.branded) return true;
|
|
16507
|
+
if (options.plain) return false;
|
|
16508
|
+
return void 0;
|
|
15738
16509
|
}
|
|
15739
|
-
function
|
|
15740
|
-
if (
|
|
15741
|
-
|
|
15742
|
-
|
|
16510
|
+
function resolveNullable(value, clear, flag) {
|
|
16511
|
+
if (value !== void 0 && clear) {
|
|
16512
|
+
throw new Error(`Use either --${flag} or --clear-${flag}, not both.`);
|
|
16513
|
+
}
|
|
16514
|
+
return clear ? null : value;
|
|
15743
16515
|
}
|
|
15744
|
-
function
|
|
15745
|
-
const
|
|
15746
|
-
|
|
16516
|
+
function buildFields(options) {
|
|
16517
|
+
const input = {};
|
|
16518
|
+
if (options.name !== void 0) input.name = options.name;
|
|
16519
|
+
if (options.subject !== void 0) input.subject = options.subject;
|
|
16520
|
+
const preheader = resolveNullable(options.preheader, options.clearPreheader, "preheader");
|
|
16521
|
+
if (preheader !== void 0) input.preheader = preheader;
|
|
16522
|
+
const contentMarkdown = resolveContent5(options);
|
|
16523
|
+
if (contentMarkdown !== void 0) input.contentMarkdown = contentMarkdown;
|
|
16524
|
+
const branded = resolveBranded2(options);
|
|
16525
|
+
if (branded !== void 0) input.branded = branded;
|
|
16526
|
+
const fromName = resolveNullable(options.fromName, options.clearFromName, "from-name");
|
|
16527
|
+
if (fromName !== void 0) input.fromName = fromName;
|
|
16528
|
+
const replyTo = resolveNullable(options.replyTo, options.clearReplyTo, "reply-to");
|
|
16529
|
+
if (replyTo !== void 0) input.replyTo = replyTo;
|
|
16530
|
+
if (options.clearAttachments) input.attachments = [];
|
|
16531
|
+
return input;
|
|
15747
16532
|
}
|
|
15748
|
-
function
|
|
15749
|
-
|
|
15750
|
-
const raw = trimmed.startsWith("[") ? trimmed : readFileSync10(value, "utf8");
|
|
15751
|
-
let parsed;
|
|
15752
|
-
try {
|
|
15753
|
-
parsed = JSON.parse(raw);
|
|
15754
|
-
} catch (error) {
|
|
15755
|
-
throw new Error(`Invalid JSON for --layout: ${error.message}`);
|
|
15756
|
-
}
|
|
15757
|
-
if (!Array.isArray(parsed)) {
|
|
15758
|
-
throw new Error("--layout must be a JSON array of email blocks (the full layout).");
|
|
15759
|
-
}
|
|
15760
|
-
return parsed;
|
|
16533
|
+
function describeAttachment(attachment) {
|
|
16534
|
+
return attachment.kind === "legal-document" ? `${attachment.document} (PDF rendered at send time)` : `${attachment.name} (media ${attachment.mediaId})`;
|
|
15761
16535
|
}
|
|
15762
|
-
function
|
|
15763
|
-
|
|
16536
|
+
function printLibraryEmail(email) {
|
|
16537
|
+
process.stdout.write(`E-mail template ${email.id}
|
|
16538
|
+
`);
|
|
16539
|
+
process.stdout.write(` Name: ${email.name}
|
|
16540
|
+
`);
|
|
16541
|
+
process.stdout.write(` Subject: ${email.subject || "(none)"}
|
|
16542
|
+
`);
|
|
16543
|
+
process.stdout.write(` Preheader: ${email.preheader ?? "(none)"}
|
|
16544
|
+
`);
|
|
16545
|
+
process.stdout.write(` Frame: ${email.branded ? "branded" : "plain"}
|
|
16546
|
+
`);
|
|
16547
|
+
process.stdout.write(` From name: ${email.fromName ?? "(workspace default)"}
|
|
16548
|
+
`);
|
|
16549
|
+
process.stdout.write(` Reply-To: ${email.replyTo ?? "(workspace default)"}
|
|
16550
|
+
`);
|
|
16551
|
+
process.stdout.write(
|
|
16552
|
+
` Attachments: ${email.attachments.length > 0 ? email.attachments.map(describeAttachment).join(", ") : "(none)"}
|
|
16553
|
+
`
|
|
16554
|
+
);
|
|
16555
|
+
process.stdout.write(` Updated at: ${email.updatedAt}
|
|
16556
|
+
`);
|
|
16557
|
+
process.stdout.write(` Content (markdown, ${email.contentMarkdown.length} chars):
|
|
16558
|
+
`);
|
|
16559
|
+
if (email.contentMarkdown) {
|
|
16560
|
+
for (const line of email.contentMarkdown.split("\n")) process.stdout.write(` ${line}
|
|
16561
|
+
`);
|
|
16562
|
+
}
|
|
16563
|
+
}
|
|
16564
|
+
var FIELD_FLAGS_HELP = "Attachments other than clearing them are not settable from flags \u2014 attach files in the dashboard.";
|
|
16565
|
+
function registerEmailLibraryCommands(emails) {
|
|
16566
|
+
const library = emails.command("library").description(
|
|
16567
|
+
'The reusable e-mail template library (Marketing \u2192 E-mail templates) that workflow "Send email" steps reference \u2014 not the system e-mails above'
|
|
16568
|
+
);
|
|
16569
|
+
library.command("list").description("List library e-mail templates").action(async (_, command) => {
|
|
16570
|
+
const flags = getGlobalFlags(command);
|
|
16571
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
16572
|
+
const page = await client.listLibraryEmails();
|
|
16573
|
+
emit(flags, page, () => {
|
|
16574
|
+
printTable(
|
|
16575
|
+
page.emails.map((e) => ({
|
|
16576
|
+
id: e.id,
|
|
16577
|
+
name: e.name,
|
|
16578
|
+
subject: e.subject,
|
|
16579
|
+
frame: e.branded ? "branded" : "plain",
|
|
16580
|
+
attachments: e.attachments.length,
|
|
16581
|
+
updatedAt: e.updatedAt
|
|
16582
|
+
})),
|
|
16583
|
+
["id", "name", "subject", "frame", "attachments", "updatedAt"]
|
|
16584
|
+
);
|
|
16585
|
+
});
|
|
16586
|
+
});
|
|
16587
|
+
library.command("get").description("Show one library e-mail template (subject, sender, attachments, content)").argument("<id>", "e-mail template id").action(async (id, _options, command) => {
|
|
16588
|
+
const flags = getGlobalFlags(command);
|
|
16589
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
16590
|
+
const result = await client.getLibraryEmail(id);
|
|
16591
|
+
emit(flags, result, () => printLibraryEmail(result.email));
|
|
16592
|
+
});
|
|
16593
|
+
library.command("create").description(`Create a library e-mail template. ${FIELD_FLAGS_HELP}`).requiredOption("--name <name>", "internal label shown in the library list (not the subject)").option("--subject <subject>", "e-mail subject line").option("--preheader <text>", "preview text shown after the subject in inboxes").option("--content <markdown>", "e-mail body as Markdown").option("--content-file <path>", "read the e-mail body (Markdown) from a file").option("--branded", "wrap the e-mail in the workspace's branded frame").option("--plain", "send it as a plain, personal-looking e-mail").option("--from-name <name>", "From display name override").option("--reply-to <email>", "Reply-To address override").action(async (options, command) => {
|
|
16594
|
+
const flags = getGlobalFlags(command);
|
|
16595
|
+
const fields = buildFields(options);
|
|
16596
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
16597
|
+
const result = await client.createLibraryEmail({ ...fields, name: options.name });
|
|
16598
|
+
emit(flags, result, () => {
|
|
16599
|
+
process.stdout.write(
|
|
16600
|
+
`Created e-mail template ${result.email.id} (${result.email.name}).
|
|
16601
|
+
`
|
|
16602
|
+
);
|
|
16603
|
+
});
|
|
16604
|
+
});
|
|
16605
|
+
library.command("update").description(
|
|
16606
|
+
`Update a library e-mail template \u2014 workflows that reference it send the new version from now on. ${FIELD_FLAGS_HELP}`
|
|
16607
|
+
).argument("<id>", "e-mail template id").option("--name <name>", "new internal label").option("--subject <subject>", "new subject line").option("--preheader <text>", "new preview text").option("--clear-preheader", "remove the preview text").option("--content <markdown>", "new body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new body (Markdown) from a file").option("--branded", "wrap the e-mail in the workspace's branded frame").option("--plain", "send it as a plain, personal-looking e-mail").option("--from-name <name>", "new From display name override").option("--clear-from-name", "go back to the workspace default From name").option("--reply-to <email>", "new Reply-To address override").option("--clear-reply-to", "go back to the workspace default Reply-To").option("--clear-attachments", "remove every attachment").action(async (id, options, command) => {
|
|
16608
|
+
const flags = getGlobalFlags(command);
|
|
16609
|
+
const input = buildFields(options);
|
|
16610
|
+
if (Object.keys(input).length === 0) {
|
|
16611
|
+
throw new Error(
|
|
16612
|
+
"Nothing to update \u2014 pass at least one change flag (see `emails library update --help`)."
|
|
16613
|
+
);
|
|
16614
|
+
}
|
|
16615
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
16616
|
+
const result = await client.updateLibraryEmail(id, input);
|
|
16617
|
+
emit(flags, result, () => {
|
|
16618
|
+
process.stdout.write(`Updated e-mail template ${result.email.id}.
|
|
16619
|
+
`);
|
|
16620
|
+
});
|
|
16621
|
+
});
|
|
16622
|
+
library.command("duplicate").description("Duplicate a library e-mail template (the copy is independent of the original)").argument("<id>", "e-mail template id").action(async (id, _options, command) => {
|
|
16623
|
+
const flags = getGlobalFlags(command);
|
|
16624
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
16625
|
+
const result = await client.duplicateLibraryEmail(id);
|
|
16626
|
+
emit(flags, result, () => {
|
|
16627
|
+
process.stdout.write(`Duplicated ${id} \u2192 ${result.email.id} (${result.email.name}).
|
|
16628
|
+
`);
|
|
16629
|
+
});
|
|
16630
|
+
});
|
|
16631
|
+
library.command("delete").description(
|
|
16632
|
+
"PERMANENTLY delete a library e-mail template \u2014 cannot be undone; a workflow step that still references it stops sending until it is re-pointed. Requires --confirm"
|
|
16633
|
+
).argument("<id>", "e-mail template id").option("--confirm", "required \u2014 acknowledge the permanent deletion").action(async (id, options, command) => {
|
|
16634
|
+
const flags = getGlobalFlags(command);
|
|
16635
|
+
if (!options.confirm) {
|
|
16636
|
+
throw new Error(
|
|
16637
|
+
`Refusing to delete e-mail template ${id} without --confirm. Re-run with --confirm to proceed.`
|
|
16638
|
+
);
|
|
16639
|
+
}
|
|
16640
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
16641
|
+
const result = await client.deleteLibraryEmail(id);
|
|
16642
|
+
emit(flags, result, () => {
|
|
16643
|
+
process.stdout.write(`Deleted e-mail template ${id}.
|
|
16644
|
+
`);
|
|
16645
|
+
});
|
|
16646
|
+
});
|
|
16647
|
+
}
|
|
16648
|
+
|
|
16649
|
+
// src/commands/emails/sender.ts
|
|
16650
|
+
function printSender(sender) {
|
|
16651
|
+
const defaultFrom = sender.senderDomain?.defaultFromEmail ?? sender.platformFromAddress;
|
|
16652
|
+
process.stdout.write(`Default From: ${defaultFrom}`);
|
|
16653
|
+
process.stdout.write(sender.senderDomain ? "\n" : " (platform sender \u2014 no custom domain)\n");
|
|
16654
|
+
process.stdout.write(
|
|
16655
|
+
`Footer postal address: ${sender.postalAddress ?? "(not set \u2014 campaign sends require one)"}
|
|
16656
|
+
`
|
|
16657
|
+
);
|
|
16658
|
+
process.stdout.write(`Unsubscribe label: ${sender.unsubscribeLabel ?? "(language default)"}
|
|
16659
|
+
`);
|
|
16660
|
+
process.stdout.write(
|
|
16661
|
+
`System e-mails from the custom domain: ${sender.systemEmailsFromCustomDomain ? "yes" : "no"}
|
|
16662
|
+
`
|
|
16663
|
+
);
|
|
16664
|
+
process.stdout.write(
|
|
16665
|
+
`Sending domains: ${sender.senderDomains.length} of ${sender.maxSenderDomains}
|
|
16666
|
+
`
|
|
16667
|
+
);
|
|
16668
|
+
if (sender.senderDomains.length > 0) {
|
|
16669
|
+
printTable(
|
|
16670
|
+
sender.senderDomains.map((d) => ({
|
|
16671
|
+
domain: d.domain,
|
|
16672
|
+
default: d.isDefault ? "yes" : "",
|
|
16673
|
+
status: d.status,
|
|
16674
|
+
mailFrom: d.mailFromStatus,
|
|
16675
|
+
from: d.defaultFromEmail,
|
|
16676
|
+
verifiedAt: d.verifiedAt ?? ""
|
|
16677
|
+
})),
|
|
16678
|
+
["domain", "default", "status", "mailFrom", "from", "verifiedAt"]
|
|
16679
|
+
);
|
|
16680
|
+
}
|
|
16681
|
+
}
|
|
16682
|
+
function registerEmailSenderCommands(emails) {
|
|
16683
|
+
const sender = emails.command("sender").description(
|
|
16684
|
+
"The sending identity behind campaigns: verified sending domains, the default From and the footer postal address. Adding / verifying / deleting a domain is dashboard-only (DNS)"
|
|
16685
|
+
);
|
|
16686
|
+
sender.command("get").description("Show the sending domains, default From address and footer postal address").action(async (_, command) => {
|
|
16687
|
+
const flags = getGlobalFlags(command);
|
|
16688
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
16689
|
+
const result = await client.getSenderDomain();
|
|
16690
|
+
emit(flags, result, () => printSender(result));
|
|
16691
|
+
});
|
|
16692
|
+
sender.command("set-address").description(
|
|
16693
|
+
'Set the footer postal address rendered under the unsubscribe link of every campaign (one plain-text line, e.g. "Company, street, city, country") \u2014 required before a send'
|
|
16694
|
+
).option("--address <line>", "the postal address line (max 300 chars)").option("--clear", "remove the postal address").action(async (options, command) => {
|
|
16695
|
+
const flags = getGlobalFlags(command);
|
|
16696
|
+
if (options.address !== void 0 && options.clear) {
|
|
16697
|
+
throw new Error("Use either --address or --clear, not both.");
|
|
16698
|
+
}
|
|
16699
|
+
if (options.address === void 0 && !options.clear) {
|
|
16700
|
+
throw new Error("Pass --address <line> to set it or --clear to remove it.");
|
|
16701
|
+
}
|
|
16702
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
16703
|
+
const result = await client.updateSenderPostalAddress(
|
|
16704
|
+
options.clear ? null : options.address ?? null
|
|
16705
|
+
);
|
|
16706
|
+
emit(flags, result, () => {
|
|
16707
|
+
process.stdout.write(
|
|
16708
|
+
result.postalAddress ? `Footer postal address set to: ${result.postalAddress}
|
|
16709
|
+
` : "Footer postal address cleared.\n"
|
|
16710
|
+
);
|
|
16711
|
+
});
|
|
16712
|
+
});
|
|
16713
|
+
}
|
|
16714
|
+
|
|
16715
|
+
// src/commands/emails/index.ts
|
|
16716
|
+
var EMAIL_LANGUAGES = ["en", "pl"];
|
|
16717
|
+
var TEXT_SUMMARY_LENGTH = 60;
|
|
16718
|
+
function orNull2(value) {
|
|
16719
|
+
return value.trim().toLowerCase() === "none" ? null : value;
|
|
16720
|
+
}
|
|
16721
|
+
function parseLanguage(value, fallback) {
|
|
16722
|
+
if (value === void 0) return fallback;
|
|
16723
|
+
const normalized = value.trim().toLowerCase();
|
|
16724
|
+
if (!EMAIL_LANGUAGES.includes(normalized)) {
|
|
16725
|
+
throw new Error(`Invalid --lang: ${value} (expected ${EMAIL_LANGUAGES.join(" | ")})`);
|
|
16726
|
+
}
|
|
16727
|
+
return normalized;
|
|
16728
|
+
}
|
|
16729
|
+
function pickText(text, language) {
|
|
16730
|
+
if (!text) return "";
|
|
16731
|
+
const other = language === "en" ? "pl" : "en";
|
|
16732
|
+
return text[language] ?? text[other] ?? "";
|
|
16733
|
+
}
|
|
16734
|
+
function truncate2(value, max) {
|
|
16735
|
+
const singleLine = value.replace(/\s+/g, " ").trim();
|
|
16736
|
+
return singleLine.length > max ? `${singleLine.slice(0, max - 1)}\u2026` : singleLine;
|
|
16737
|
+
}
|
|
16738
|
+
function parseLayout(value) {
|
|
16739
|
+
const trimmed = value.trim();
|
|
16740
|
+
const raw = trimmed.startsWith("[") ? trimmed : readFileSync12(value, "utf8");
|
|
16741
|
+
let parsed;
|
|
16742
|
+
try {
|
|
16743
|
+
parsed = JSON.parse(raw);
|
|
16744
|
+
} catch (error) {
|
|
16745
|
+
throw new Error(`Invalid JSON for --layout: ${error.message}`);
|
|
16746
|
+
}
|
|
16747
|
+
if (!Array.isArray(parsed)) {
|
|
16748
|
+
throw new Error("--layout must be a JSON array of email blocks (the full layout).");
|
|
16749
|
+
}
|
|
16750
|
+
return parsed;
|
|
16751
|
+
}
|
|
16752
|
+
function findEmail(templates, key) {
|
|
16753
|
+
const email = templates.emails.find((entry) => entry.key === key);
|
|
15764
16754
|
if (!email) {
|
|
15765
16755
|
const known = templates.emails.map((entry) => entry.key).join(", ");
|
|
15766
16756
|
throw new Error(`Unknown email "${key}". Known keys: ${known}`);
|
|
@@ -15783,7 +16773,7 @@ function summarizeBlock(block, language) {
|
|
|
15783
16773
|
return "";
|
|
15784
16774
|
}
|
|
15785
16775
|
}
|
|
15786
|
-
function
|
|
16776
|
+
function printEmailDetail2(email, language) {
|
|
15787
16777
|
printTable(
|
|
15788
16778
|
[
|
|
15789
16779
|
{ field: "key", value: email.key },
|
|
@@ -15896,7 +16886,7 @@ function registerEmailsCommands(program2) {
|
|
|
15896
16886
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
15897
16887
|
const templates = await client.getEmailTemplates();
|
|
15898
16888
|
const email = findEmail(templates, key);
|
|
15899
|
-
emit(flags, email, () =>
|
|
16889
|
+
emit(flags, email, () => printEmailDetail2(email, templates.defaults.language));
|
|
15900
16890
|
});
|
|
15901
16891
|
emails.command("set <key>").description(
|
|
15902
16892
|
"Customize one system email. Text flags take 'none' to clear; --layout replaces the whole block layout (inline JSON array or .json path); --reset restores the stock email."
|
|
@@ -15970,10 +16960,12 @@ function registerEmailsCommands(program2) {
|
|
|
15970
16960
|
`);
|
|
15971
16961
|
});
|
|
15972
16962
|
});
|
|
16963
|
+
registerEmailLibraryCommands(emails);
|
|
16964
|
+
registerEmailSenderCommands(emails);
|
|
15973
16965
|
}
|
|
15974
16966
|
|
|
15975
16967
|
// src/commands/forms/index.ts
|
|
15976
|
-
import { readFileSync as
|
|
16968
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
15977
16969
|
var OPT_IN_MODES = ["double", "single"];
|
|
15978
16970
|
var FORM_VARIANTS = [
|
|
15979
16971
|
"basic",
|
|
@@ -15990,7 +16982,7 @@ function parseChoice2(value, allowed, flag) {
|
|
|
15990
16982
|
}
|
|
15991
16983
|
return match;
|
|
15992
16984
|
}
|
|
15993
|
-
function
|
|
16985
|
+
function parsePositiveInt9(value, flag) {
|
|
15994
16986
|
const n = Number.parseInt(value, 10);
|
|
15995
16987
|
if (!Number.isInteger(n) || n <= 0) {
|
|
15996
16988
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
@@ -16005,13 +16997,16 @@ function parseCsv2(value) {
|
|
|
16005
16997
|
}
|
|
16006
16998
|
function parseJsonFileOrInline3(value, flag) {
|
|
16007
16999
|
const trimmed = value.trim();
|
|
16008
|
-
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed :
|
|
17000
|
+
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync13(value, "utf8");
|
|
16009
17001
|
try {
|
|
16010
17002
|
return JSON.parse(raw);
|
|
16011
17003
|
} catch (error) {
|
|
16012
17004
|
throw new Error(`Invalid JSON for ${flag}: ${error.message}`);
|
|
16013
17005
|
}
|
|
16014
17006
|
}
|
|
17007
|
+
var PHONE_FIELD_MODES = ["off", "optional", "required"];
|
|
17008
|
+
var ON_OFF = ["on", "off"];
|
|
17009
|
+
var MAX_SMS_CONSENT_TEXT_LENGTH = 300;
|
|
16015
17010
|
function buildConfigOverrides(options) {
|
|
16016
17011
|
const overrides = {};
|
|
16017
17012
|
if (options.header !== void 0) overrides.headerText = options.header;
|
|
@@ -16021,8 +17016,32 @@ function buildConfigOverrides(options) {
|
|
|
16021
17016
|
if (options.variant !== void 0) {
|
|
16022
17017
|
overrides.variant = parseChoice2(options.variant, FORM_VARIANTS, "--variant");
|
|
16023
17018
|
}
|
|
17019
|
+
if (options.phone !== void 0) {
|
|
17020
|
+
const mode = parseChoice2(options.phone, PHONE_FIELD_MODES, "--phone");
|
|
17021
|
+
overrides.showPhoneNumberInput = mode !== "off";
|
|
17022
|
+
overrides.phoneNumberRequired = mode === "required";
|
|
17023
|
+
if (mode === "off") overrides.showSmsConsentCheckbox = false;
|
|
17024
|
+
}
|
|
17025
|
+
if (options.smsConsent !== void 0) {
|
|
17026
|
+
overrides.showSmsConsentCheckbox = parseChoice2(options.smsConsent, ON_OFF, "--sms-consent") === "on";
|
|
17027
|
+
}
|
|
17028
|
+
if (options.smsConsentText !== void 0) {
|
|
17029
|
+
if (options.smsConsentText.length > MAX_SMS_CONSENT_TEXT_LENGTH) {
|
|
17030
|
+
throw new Error(
|
|
17031
|
+
`--sms-consent-text is limited to ${MAX_SMS_CONSENT_TEXT_LENGTH} characters.`
|
|
17032
|
+
);
|
|
17033
|
+
}
|
|
17034
|
+
overrides.smsConsentText = options.smsConsentText;
|
|
17035
|
+
}
|
|
16024
17036
|
return Object.keys(overrides).length > 0 ? overrides : void 0;
|
|
16025
17037
|
}
|
|
17038
|
+
function assertSmsConsentHasPhoneField(config) {
|
|
17039
|
+
if (config?.showSmsConsentCheckbox === true && config.showPhoneNumberInput !== true) {
|
|
17040
|
+
throw new Error(
|
|
17041
|
+
"--sms-consent on needs the phone field: add --phone optional or --phone required."
|
|
17042
|
+
);
|
|
17043
|
+
}
|
|
17044
|
+
}
|
|
16026
17045
|
function parseFormJsonPatch(value) {
|
|
16027
17046
|
const parsed = parseJsonFileOrInline3(value, "--json");
|
|
16028
17047
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
@@ -16084,6 +17103,7 @@ async function buildFormUpdateInput(client, formId, options) {
|
|
|
16084
17103
|
if (configOverrides !== void 0) {
|
|
16085
17104
|
const base = input.config ?? (await fetchCurrent()).config;
|
|
16086
17105
|
input.config = { ...base, ...configOverrides };
|
|
17106
|
+
assertSmsConsentHasPhoneField(input.config);
|
|
16087
17107
|
}
|
|
16088
17108
|
if (options.clearConfirmationEmail) {
|
|
16089
17109
|
if (options.confirmationSubject !== void 0 || options.confirmationIntro !== void 0) {
|
|
@@ -16135,19 +17155,30 @@ function registerFormsCommands(program2) {
|
|
|
16135
17155
|
emit(flags, form, () => printSignupFormDetail(form));
|
|
16136
17156
|
});
|
|
16137
17157
|
forms.command("create").description(
|
|
16138
|
-
"Create a standalone signup form (double opt-in
|
|
16139
|
-
).requiredOption("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids applied on (confirmed) submission").option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).
|
|
17158
|
+
"Create a standalone signup form (double opt-in unless --opt-in single). The full confirmation-email copy is set with `forms update --json`"
|
|
17159
|
+
).requiredOption("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids applied on (confirmed) submission").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")}; default double)`).option("--redirect <url>", "thank-you redirect after a successful submit").option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option("--phone <mode>", `config: phone number field (${PHONE_FIELD_MODES.join(" | ")})`).option(
|
|
17160
|
+
"--sms-consent <on|off>",
|
|
17161
|
+
"config: marketing-SMS consent checkbox (needs the phone field)"
|
|
17162
|
+
).option(
|
|
17163
|
+
"--sms-consent-text <text>",
|
|
17164
|
+
"config: consent wording next to the checkbox (max 300 chars)"
|
|
17165
|
+
).action(
|
|
16140
17166
|
async (options, command) => {
|
|
16141
17167
|
const flags = getGlobalFlags(command);
|
|
17168
|
+
const optInMode = options.optIn !== void 0 ? parseChoice2(options.optIn, OPT_IN_MODES, "--opt-in") : void 0;
|
|
17169
|
+
const config = buildConfigOverrides(options);
|
|
17170
|
+
assertSmsConsentHasPhoneField(config);
|
|
16142
17171
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
16143
17172
|
const result = await client.createSignupForm({
|
|
16144
17173
|
name: options.name,
|
|
16145
17174
|
tagIds: options.tags !== void 0 ? parseCsv2(options.tags) : void 0,
|
|
16146
|
-
config
|
|
17175
|
+
config,
|
|
17176
|
+
...optInMode !== void 0 ? { optInMode } : {},
|
|
17177
|
+
...options.redirect !== void 0 ? { redirectUrl: options.redirect } : {}
|
|
16147
17178
|
});
|
|
16148
17179
|
emit(flags, result, () => {
|
|
16149
17180
|
process.stdout.write(
|
|
16150
|
-
`Created signup form ${result.formId} (${options.name}) \u2014 double opt-in
|
|
17181
|
+
`Created signup form ${result.formId} (${options.name}) \u2014 ${optInMode ?? "double"} opt-in.
|
|
16151
17182
|
`
|
|
16152
17183
|
);
|
|
16153
17184
|
});
|
|
@@ -16155,7 +17186,13 @@ function registerFormsCommands(program2) {
|
|
|
16155
17186
|
);
|
|
16156
17187
|
forms.command("update").description(
|
|
16157
17188
|
"Update a standalone form. The embed config is REPLACED IN FULL by the API \u2014 flat config flags are merged over the current config for you; a --json `config` object is sent as the full replacement (flat flags still win). `none` clears nullable fields."
|
|
16158
|
-
).argument("<id>", "form id").option("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids (full replace; empty string clears)").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")})`).option("--redirect <url|none>", "thank-you redirect after submit; `none` clears it").option("--confirmation-subject <text>", "double-opt-in confirmation email subject override").option("--confirmation-intro <text>", "double-opt-in confirmation email intro override").option("--clear-confirmation-email", "restore the workspace default confirmation copy").option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option(
|
|
17189
|
+
).argument("<id>", "form id").option("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids (full replace; empty string clears)").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")})`).option("--redirect <url|none>", "thank-you redirect after submit; `none` clears it").option("--confirmation-subject <text>", "double-opt-in confirmation email subject override").option("--confirmation-intro <text>", "double-opt-in confirmation email intro override").option("--clear-confirmation-email", "restore the workspace default confirmation copy").option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option("--phone <mode>", `config: phone number field (${PHONE_FIELD_MODES.join(" | ")})`).option(
|
|
17190
|
+
"--sms-consent <on|off>",
|
|
17191
|
+
"config: marketing-SMS consent checkbox (needs the phone field)"
|
|
17192
|
+
).option(
|
|
17193
|
+
"--sms-consent-text <text>",
|
|
17194
|
+
"config: consent wording next to the checkbox (max 300 chars)"
|
|
17195
|
+
).option(
|
|
16159
17196
|
"--json <fileOrJson>",
|
|
16160
17197
|
"inline JSON object or .json path merged into the request body \u2014 for deep fields (config, localizedConfig, \u2026). Explicit flags win over --json keys."
|
|
16161
17198
|
).action(async (id, options, command) => {
|
|
@@ -16189,7 +17226,7 @@ function registerFormsCommands(program2) {
|
|
|
16189
17226
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
16190
17227
|
const result = await client.listSignupFormSubmissions(
|
|
16191
17228
|
id,
|
|
16192
|
-
options.limit !== void 0 ?
|
|
17229
|
+
options.limit !== void 0 ? parsePositiveInt9(options.limit, "--limit") : void 0
|
|
16193
17230
|
);
|
|
16194
17231
|
emit(flags, result, () => {
|
|
16195
17232
|
printTable(
|
|
@@ -16207,7 +17244,7 @@ function registerFormsCommands(program2) {
|
|
|
16207
17244
|
}
|
|
16208
17245
|
|
|
16209
17246
|
// src/commands/funnels/index.ts
|
|
16210
|
-
import { readFileSync as
|
|
17247
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
16211
17248
|
var CREATABLE_RECIPES = ["lead_magnet", "tripwire", "sell_product"];
|
|
16212
17249
|
var FUNNEL_STATUSES = ["draft", "live", "archived"];
|
|
16213
17250
|
var FUNNEL_LANGS = ["en", "pl"];
|
|
@@ -16215,12 +17252,13 @@ var FUNNEL_TEMPLATE_IDS = [
|
|
|
16215
17252
|
"squeeze",
|
|
16216
17253
|
"tripwire",
|
|
16217
17254
|
"sales_oto",
|
|
17255
|
+
"low_ticket",
|
|
16218
17256
|
"webinar",
|
|
16219
17257
|
"challenge"
|
|
16220
17258
|
];
|
|
16221
17259
|
var EVENT_MODES = ["live", "on_demand"];
|
|
16222
17260
|
var SCHEDULER_UNAVAILABLE_STATUS2 = 503;
|
|
16223
|
-
function
|
|
17261
|
+
function requireConfirm6(confirm, what) {
|
|
16224
17262
|
if (!confirm) {
|
|
16225
17263
|
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
16226
17264
|
}
|
|
@@ -16235,7 +17273,7 @@ function withSchedulerHint2(error) {
|
|
|
16235
17273
|
}
|
|
16236
17274
|
throw error;
|
|
16237
17275
|
}
|
|
16238
|
-
function
|
|
17276
|
+
function parsePositiveInt10(value, flag) {
|
|
16239
17277
|
const n = Number.parseInt(value, 10);
|
|
16240
17278
|
if (!Number.isInteger(n) || n <= 0) {
|
|
16241
17279
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
@@ -16272,7 +17310,7 @@ function parseCsv3(value) {
|
|
|
16272
17310
|
}
|
|
16273
17311
|
function parseJsonFileOrInline4(value, flag) {
|
|
16274
17312
|
const trimmed = value.trim();
|
|
16275
|
-
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed :
|
|
17313
|
+
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync14(value, "utf8");
|
|
16276
17314
|
try {
|
|
16277
17315
|
return JSON.parse(raw);
|
|
16278
17316
|
} catch (error) {
|
|
@@ -16495,7 +17533,7 @@ function registerFunnelsCommands(program2) {
|
|
|
16495
17533
|
).argument("<templateId>", `template id (${FUNNEL_TEMPLATE_IDS.join(" | ")})`).requiredOption("--product <productId>", "the product the funnel sells or delivers").requiredOption("--lang <en|pl>", "language of the generated pages and emails \u2014 the creator's").option("--name <name>", "custom funnel name; omit for the recipe default").option("--event-start <iso>", "webinar: event start as ISO 8601 WITH offset (live)").option("--event-timezone <tz>", "webinar: IANA timezone, e.g. Europe/Warsaw").option("--event-duration <minutes>", "webinar: event length in minutes (default 60)").option("--event-mode <mode>", `webinar: ${EVENT_MODES.join(" | ")} (on_demand = autowebinar)`).option("--confirm", "required \u2014 acknowledge that AI credits will be consumed").action(
|
|
16496
17534
|
async (templateId, options, command) => {
|
|
16497
17535
|
const flags = getGlobalFlags(command);
|
|
16498
|
-
|
|
17536
|
+
requireConfirm6(options.confirm, "create the funnel and generate its pages (AI credits)");
|
|
16499
17537
|
const input = {
|
|
16500
17538
|
templateId: parseChoice3(templateId, FUNNEL_TEMPLATE_IDS, "<templateId>"),
|
|
16501
17539
|
productId: options.product,
|
|
@@ -16505,7 +17543,7 @@ function registerFunnelsCommands(program2) {
|
|
|
16505
17543
|
const webinar = {
|
|
16506
17544
|
eventStartAt: options.eventStart,
|
|
16507
17545
|
eventTimezone: options.eventTimezone,
|
|
16508
|
-
eventDurationMinutes: options.eventDuration !== void 0 ?
|
|
17546
|
+
eventDurationMinutes: options.eventDuration !== void 0 ? parsePositiveInt10(options.eventDuration, "--event-duration") : void 0,
|
|
16509
17547
|
eventMode: options.eventMode !== void 0 ? parseChoice3(options.eventMode, EVENT_MODES, "--event-mode") : void 0
|
|
16510
17548
|
};
|
|
16511
17549
|
if (Object.values(webinar).some((value) => value !== void 0)) {
|
|
@@ -16753,7 +17791,7 @@ join again to refresh it.
|
|
|
16753
17791
|
}
|
|
16754
17792
|
|
|
16755
17793
|
// src/commands/media/index.ts
|
|
16756
|
-
import { existsSync, readFileSync as
|
|
17794
|
+
import { existsSync, readFileSync as readFileSync15, writeFileSync as writeFileSync4 } from "fs";
|
|
16757
17795
|
import { readFile, stat as stat3 } from "fs/promises";
|
|
16758
17796
|
import { basename } from "path";
|
|
16759
17797
|
|
|
@@ -19172,11 +20210,11 @@ function _readChunk() {
|
|
|
19172
20210
|
reject(err);
|
|
19173
20211
|
};
|
|
19174
20212
|
var onReadable = function onReadable2() {
|
|
19175
|
-
var
|
|
19176
|
-
if (
|
|
20213
|
+
var chunk2 = stream.read(size);
|
|
20214
|
+
if (chunk2 !== null) {
|
|
19177
20215
|
stream.off("error", onError);
|
|
19178
20216
|
stream.off("readable", onReadable2);
|
|
19179
|
-
resolve2(
|
|
20217
|
+
resolve2(chunk2);
|
|
19180
20218
|
}
|
|
19181
20219
|
};
|
|
19182
20220
|
stream.once("error", onError);
|
|
@@ -19658,10 +20696,10 @@ var ProgressEmitter = /* @__PURE__ */ (function(_Transform) {
|
|
|
19658
20696
|
_inherits2(ProgressEmitter2, _Transform);
|
|
19659
20697
|
return _createClass8(ProgressEmitter2, [{
|
|
19660
20698
|
key: "_transform",
|
|
19661
|
-
value: function _transform(
|
|
19662
|
-
this._position +=
|
|
20699
|
+
value: function _transform(chunk2, _encoding, callback) {
|
|
20700
|
+
this._position += chunk2.length;
|
|
19663
20701
|
this._onprogress(this._position);
|
|
19664
|
-
callback(null,
|
|
20702
|
+
callback(null, chunk2);
|
|
19665
20703
|
}
|
|
19666
20704
|
}]);
|
|
19667
20705
|
})(Transform);
|
|
@@ -19673,9 +20711,9 @@ var writeBufferToStreamWithProgress = function writeBufferToStreamWithProgress2(
|
|
|
19673
20711
|
var offset = 0;
|
|
19674
20712
|
function writeNextChunk() {
|
|
19675
20713
|
var chunkSize = Math.min(stream.writableHighWaterMark, source.length - offset);
|
|
19676
|
-
var
|
|
19677
|
-
offset +=
|
|
19678
|
-
var canContinue = stream.write(
|
|
20714
|
+
var chunk2 = source.subarray(offset, offset + chunkSize);
|
|
20715
|
+
offset += chunk2.length;
|
|
20716
|
+
var canContinue = stream.write(chunk2);
|
|
19679
20717
|
if (!canContinue) {
|
|
19680
20718
|
stream.once("drain", writeNextChunk);
|
|
19681
20719
|
onprogress(offset);
|
|
@@ -19874,13 +20912,13 @@ async function uploadVideoToBunny(params) {
|
|
|
19874
20912
|
// src/commands/media/index.ts
|
|
19875
20913
|
var DEFAULT_AI_IMAGE_ASPECT = "1:1" /* Square */;
|
|
19876
20914
|
var DEFAULT_AI_IMAGE_QUALITY = "1K" /* Standard */;
|
|
19877
|
-
function
|
|
20915
|
+
function requireConfirm7(confirm, what) {
|
|
19878
20916
|
if (!confirm) {
|
|
19879
20917
|
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
19880
20918
|
}
|
|
19881
20919
|
}
|
|
19882
20920
|
function resolveTextOrFile(value) {
|
|
19883
|
-
const text = existsSync(value) ?
|
|
20921
|
+
const text = existsSync(value) ? readFileSync15(value, "utf8") : value;
|
|
19884
20922
|
if (!text.trim()) throw new Error("The text (or the file it points to) is empty.");
|
|
19885
20923
|
return text.trim();
|
|
19886
20924
|
}
|
|
@@ -20185,7 +21223,7 @@ function registerMediaCommands(program2) {
|
|
|
20185
21223
|
).option("--folder <name>", "media library folder name to file the image into").option("--text-language <code>", "language of any text rendered inside the image (ISO code)").option("--confirm", "required \u2014 acknowledge spending AI credits").action(
|
|
20186
21224
|
async (options, command) => {
|
|
20187
21225
|
const flags = getGlobalFlags(command);
|
|
20188
|
-
|
|
21226
|
+
requireConfirm7(options.confirm, "generate an AI image (spends AI credits)");
|
|
20189
21227
|
const input = {
|
|
20190
21228
|
prompt: resolveTextOrFile(options.prompt),
|
|
20191
21229
|
aspectRatio: parseEnumOption(
|
|
@@ -20217,7 +21255,7 @@ function registerMediaCommands(program2) {
|
|
|
20217
21255
|
).argument("<mediaId>", "video media item id").option("--language <code>", "spoken language hint (ISO code); omit for auto-detect").option("--force", "re-transcribe even when a transcript already exists").option("--confirm", "required \u2014 acknowledge spending transcription minutes").action(
|
|
20218
21256
|
async (mediaId, options, command) => {
|
|
20219
21257
|
const flags = getGlobalFlags(command);
|
|
20220
|
-
|
|
21258
|
+
requireConfirm7(options.confirm, `transcribe ${mediaId} (spends transcription minutes)`);
|
|
20221
21259
|
const input = {};
|
|
20222
21260
|
if (options.language !== void 0) input.language = options.language;
|
|
20223
21261
|
if (options.force) input.force = true;
|
|
@@ -20291,13 +21329,13 @@ function registerMediaCommands(program2) {
|
|
|
20291
21329
|
}
|
|
20292
21330
|
|
|
20293
21331
|
// src/commands/newsletters/index.ts
|
|
20294
|
-
import { existsSync as existsSync2, readFileSync as
|
|
21332
|
+
import { existsSync as existsSync2, readFileSync as readFileSync16 } from "fs";
|
|
20295
21333
|
var ISSUE_STATUSES = ["public", "hidden"];
|
|
20296
21334
|
var ISSUE_VISIBILITIES = ["public", "hidden", "locked"];
|
|
20297
21335
|
var ISSUE_RENDER_MODES = ["snapshot", "article"];
|
|
20298
21336
|
var THUMBNAIL_NONE = "none";
|
|
20299
21337
|
function resolveMarkdownOrFile(value) {
|
|
20300
|
-
return existsSync2(value) ?
|
|
21338
|
+
return existsSync2(value) ? readFileSync16(value, "utf8") : value;
|
|
20301
21339
|
}
|
|
20302
21340
|
function parseIdList2(value, flag) {
|
|
20303
21341
|
const ids = value.split(",").map((id) => id.trim()).filter(Boolean);
|
|
@@ -20316,7 +21354,7 @@ function assertOneOf2(value, allowed, flag) {
|
|
|
20316
21354
|
}
|
|
20317
21355
|
return value;
|
|
20318
21356
|
}
|
|
20319
|
-
function
|
|
21357
|
+
function parsePositiveInt11(value, flag) {
|
|
20320
21358
|
const n = Number.parseInt(value, 10);
|
|
20321
21359
|
if (!Number.isInteger(n) || n <= 0) {
|
|
20322
21360
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
@@ -20338,7 +21376,7 @@ function parseBool2(value, flag) {
|
|
|
20338
21376
|
}
|
|
20339
21377
|
function parseJsonFileOrInline5(value, flag) {
|
|
20340
21378
|
const trimmed = value.trim();
|
|
20341
|
-
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed :
|
|
21379
|
+
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync16(value, "utf8");
|
|
20342
21380
|
try {
|
|
20343
21381
|
return JSON.parse(raw);
|
|
20344
21382
|
} catch (error) {
|
|
@@ -20356,7 +21394,7 @@ function registerNewslettersCommands(program2) {
|
|
|
20356
21394
|
const flags = getGlobalFlags(command);
|
|
20357
21395
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
20358
21396
|
const page = await client.listNewsletterIssues(options.product, {
|
|
20359
|
-
limit: options.limit !== void 0 ?
|
|
21397
|
+
limit: options.limit !== void 0 ? parsePositiveInt11(options.limit, "--limit") : void 0,
|
|
20360
21398
|
cursor: options.cursor
|
|
20361
21399
|
});
|
|
20362
21400
|
emit(flags, page, () => {
|
|
@@ -20653,9 +21691,10 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
|
|
|
20653
21691
|
}
|
|
20654
21692
|
|
|
20655
21693
|
// src/commands/offers/index.ts
|
|
20656
|
-
import { readFileSync as
|
|
21694
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
20657
21695
|
var PRICE_BILLING_TYPES = ["one_time", "recurring", "installments"];
|
|
20658
21696
|
var PRICE_INTERVALS = ["day", "week", "month", "year"];
|
|
21697
|
+
var SALE_START_DISPLAYS = ["hidden", "countdown"];
|
|
20659
21698
|
var PRICE_CURRENCIES = ["pln", "usd", "eur", "gbp"];
|
|
20660
21699
|
var PRICE_OPERATIONS = [
|
|
20661
21700
|
"activate",
|
|
@@ -20671,7 +21710,7 @@ function parseChoice4(value, allowed, flag) {
|
|
|
20671
21710
|
}
|
|
20672
21711
|
return match;
|
|
20673
21712
|
}
|
|
20674
|
-
function
|
|
21713
|
+
function parsePositiveInt12(value, flag) {
|
|
20675
21714
|
const n = Number.parseInt(value, 10);
|
|
20676
21715
|
if (!Number.isInteger(n) || n <= 0) {
|
|
20677
21716
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
@@ -20686,7 +21725,7 @@ function parseBool3(value, flag) {
|
|
|
20686
21725
|
}
|
|
20687
21726
|
function parseJsonFileOrInline6(value, flag) {
|
|
20688
21727
|
const trimmed = value.trim();
|
|
20689
|
-
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed :
|
|
21728
|
+
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync17(value, "utf8");
|
|
20690
21729
|
try {
|
|
20691
21730
|
return JSON.parse(raw);
|
|
20692
21731
|
} catch (error) {
|
|
@@ -20705,27 +21744,29 @@ function buildPriceInput(options) {
|
|
|
20705
21744
|
throw new Error("Pass either --sale-ends-at or --sale-days, not both.");
|
|
20706
21745
|
}
|
|
20707
21746
|
const saleEndsAt = options.saleDays ? new Date(
|
|
20708
|
-
Date.now() +
|
|
21747
|
+
Date.now() + parsePositiveInt12(options.saleDays, "--sale-days") * 864e5
|
|
20709
21748
|
).toISOString() : options.saleEndsAt;
|
|
20710
21749
|
return {
|
|
20711
21750
|
type,
|
|
20712
|
-
amountCents:
|
|
21751
|
+
amountCents: parsePositiveInt12(options.amount, "--amount"),
|
|
20713
21752
|
currency: parseChoice4(options.currency.toLowerCase(), PRICE_CURRENCIES, "--currency"),
|
|
20714
21753
|
interval: options.interval ? parseChoice4(options.interval, PRICE_INTERVALS, "--interval") : void 0,
|
|
20715
|
-
intervalCount: options.intervalCount ?
|
|
20716
|
-
numberOfCycles: options.cycles ?
|
|
21754
|
+
intervalCount: options.intervalCount ? parsePositiveInt12(options.intervalCount, "--interval-count") : void 0,
|
|
21755
|
+
numberOfCycles: options.cycles ? parsePositiveInt12(options.cycles, "--cycles") : void 0,
|
|
20717
21756
|
installmentsAllowCancellation: options.allowCancellation ? true : void 0,
|
|
20718
|
-
trialPeriodDays: options.trialDays ?
|
|
21757
|
+
trialPeriodDays: options.trialDays ? parsePositiveInt12(options.trialDays, "--trial-days") : void 0,
|
|
20719
21758
|
label: options.label,
|
|
20720
21759
|
customInvoiceName: options.invoiceName,
|
|
20721
21760
|
hidden: options.hidden ? true : void 0,
|
|
20722
21761
|
description: options.description,
|
|
21762
|
+
saleStartsAt: options.saleStartsAt,
|
|
21763
|
+
saleStartDisplay: options.saleStartDisplay ? parseChoice4(options.saleStartDisplay, SALE_START_DISPLAYS, "--sale-start-display") : void 0,
|
|
20723
21764
|
saleEndsAt,
|
|
20724
|
-
quantityLimit: options.quantityLimit ?
|
|
21765
|
+
quantityLimit: options.quantityLimit ? parsePositiveInt12(options.quantityLimit, "--quantity-limit") : void 0,
|
|
20725
21766
|
showQuantityAvailable: options.showAvailable ? true : void 0,
|
|
20726
|
-
compareAtAmountCents: options.compareAt ?
|
|
21767
|
+
compareAtAmountCents: options.compareAt ? parsePositiveInt12(options.compareAt, "--compare-at") : void 0,
|
|
20727
21768
|
accessInterval: options.accessInterval ? parseChoice4(options.accessInterval, PRICE_INTERVALS, "--access-interval") : void 0,
|
|
20728
|
-
accessIntervalCount: options.accessCount ?
|
|
21769
|
+
accessIntervalCount: options.accessCount ? parsePositiveInt12(options.accessCount, "--access-count") : void 0,
|
|
20729
21770
|
accessUntil: options.accessUntil || void 0,
|
|
20730
21771
|
planId: options.plan
|
|
20731
21772
|
};
|
|
@@ -20784,7 +21825,7 @@ function buildOfferUpdateInput(options) {
|
|
|
20784
21825
|
set("language", options.language === void 0 ? void 0 : orNull4(options.language));
|
|
20785
21826
|
set(
|
|
20786
21827
|
"checkoutMediaWidth",
|
|
20787
|
-
options.checkoutMediaWidth === void 0 ? void 0 : options.checkoutMediaWidth.trim().toLowerCase() === "none" ? null :
|
|
21828
|
+
options.checkoutMediaWidth === void 0 ? void 0 : options.checkoutMediaWidth.trim().toLowerCase() === "none" ? null : parsePositiveInt12(options.checkoutMediaWidth, "--checkout-media-width")
|
|
20788
21829
|
);
|
|
20789
21830
|
set("showTestimonials", bool(options.showTestimonials, "--show-testimonials"));
|
|
20790
21831
|
set("showPlanIncludes", bool(options.showPlanIncludes, "--show-plan-includes"));
|
|
@@ -20848,7 +21889,7 @@ function buildOfferUpdateInput(options) {
|
|
|
20848
21889
|
set("enableClientToBuyMultipleTimes", bool(options.multiplePurchases, "--multiple-purchases"));
|
|
20849
21890
|
set(
|
|
20850
21891
|
"clientMultiplePurchaseLimit",
|
|
20851
|
-
options.multiplePurchaseLimit === void 0 ? void 0 : options.multiplePurchaseLimit.trim().toLowerCase() === "none" ? null :
|
|
21892
|
+
options.multiplePurchaseLimit === void 0 ? void 0 : options.multiplePurchaseLimit.trim().toLowerCase() === "none" ? null : parsePositiveInt12(options.multiplePurchaseLimit, "--multiple-purchase-limit")
|
|
20852
21893
|
);
|
|
20853
21894
|
set("allowUpgradeSubscriptionPlan", bool(options.allowPlanUpgrade, "--allow-plan-upgrade"));
|
|
20854
21895
|
set("allowDowngradeSubscriptionPlan", bool(options.allowPlanDowngrade, "--allow-plan-downgrade"));
|
|
@@ -20980,10 +22021,20 @@ Prices (${checkout.prices.length}):
|
|
|
20980
22021
|
amount: formatAmount2(p.amountCents, p.currency),
|
|
20981
22022
|
active: yesNo(p.active),
|
|
20982
22023
|
hidden: yesNo(p.hidden),
|
|
22024
|
+
saleStartsAt: p.saleStartsAt ?? "",
|
|
20983
22025
|
saleEndsAt: p.saleEndsAt ?? "",
|
|
20984
22026
|
quantityLimit: p.quantityLimit ?? ""
|
|
20985
22027
|
})),
|
|
20986
|
-
[
|
|
22028
|
+
[
|
|
22029
|
+
"id",
|
|
22030
|
+
"label",
|
|
22031
|
+
"amount",
|
|
22032
|
+
"active",
|
|
22033
|
+
"hidden",
|
|
22034
|
+
"saleStartsAt",
|
|
22035
|
+
"saleEndsAt",
|
|
22036
|
+
"quantityLimit"
|
|
22037
|
+
]
|
|
20987
22038
|
);
|
|
20988
22039
|
process.stdout.write(`
|
|
20989
22040
|
Order bumps (${checkout.orderBumps.length}):
|
|
@@ -21183,7 +22234,13 @@ Order bumps (${checkout.orderBumps.length}):
|
|
|
21183
22234
|
price.command("add").description("Add a price variant to an offer").argument("<checkoutId>", "offer (checkout) id").requiredOption("--type <type>", "one_time | recurring | installments").requiredOption(
|
|
21184
22235
|
"--amount <minor-units>",
|
|
21185
22236
|
"unit price in minor units, e.g. 1999 = 19.99 (installments: each payment)"
|
|
21186
|
-
).requiredOption("--currency <code>", "pln | usd | eur | gbp").option("--interval <interval>", "billing interval: day | week | month | year").option("--interval-count <n>", "bill every n intervals").option("--cycles <n>", "installments: total number of payments (>= 2)").option("--allow-cancellation", "installments: let the buyer cancel the schedule").option("--trial-days <n>", "recurring: free-trial days before the first charge").option("--label <text>", "display label").option("--invoice-name <text>", "custom invoice line name").option("--hidden", "hide from the public price list (buyable via direct link)").option("--description <text>", "markdown description rendered under the price").option(
|
|
22237
|
+
).requiredOption("--currency <code>", "pln | usd | eur | gbp").option("--interval <interval>", "billing interval: day | week | month | year").option("--interval-count <n>", "bill every n intervals").option("--cycles <n>", "installments: total number of payments (>= 2)").option("--allow-cancellation", "installments: let the buyer cancel the schedule").option("--trial-days <n>", "recurring: free-trial days before the first charge").option("--label <text>", "display label").option("--invoice-name <text>", "custom invoice line name").option("--hidden", "hide from the public price list (buyable via direct link)").option("--description <text>", "markdown description rendered under the price").option(
|
|
22238
|
+
"--sale-starts-at <ISO>",
|
|
22239
|
+
"sale start (ISO 8601): the price stays hidden and unbuyable until then"
|
|
22240
|
+
).option(
|
|
22241
|
+
"--sale-start-display <mode>",
|
|
22242
|
+
"before the start: hidden (default) | countdown (disabled tile with a countdown); needs --sale-starts-at"
|
|
22243
|
+
).option("--sale-ends-at <ISO>", "time-limited sale deadline (ISO 8601, in the future)").option("--sale-days <n>", "convenience: end the sale n days from now").option("--quantity-limit <n>", "seat limit").option("--show-available", "show the remaining quantity publicly").option("--compare-at <minor-units>", 'promo strike-through "was" price in minor units').option("--access-interval <interval>", "one-time: limited access window unit").option("--access-count <n>", "one-time: limited access window length").option(
|
|
21187
22244
|
"--access-until <iso>",
|
|
21188
22245
|
"one-time: fixed access end for every buyer (ISO 8601, future; instead of --access-interval)"
|
|
21189
22246
|
).option("--plan <planId>", "link the price to a plan").action(async (checkoutId, options, command) => {
|
|
@@ -21208,7 +22265,7 @@ Order bumps (${checkout.orderBumps.length}):
|
|
|
21208
22265
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
21209
22266
|
const result = await client.updateCheckoutPrice(checkoutId, priceId, {
|
|
21210
22267
|
operation,
|
|
21211
|
-
newAmountCents: operation === "change-amount" && options.amount ?
|
|
22268
|
+
newAmountCents: operation === "change-amount" && options.amount ? parsePositiveInt12(options.amount, "--amount") : void 0
|
|
21212
22269
|
});
|
|
21213
22270
|
emit(flags, result, () => {
|
|
21214
22271
|
process.stdout.write(
|
|
@@ -21259,7 +22316,7 @@ function assertOrderStatus(status) {
|
|
|
21259
22316
|
}
|
|
21260
22317
|
return status;
|
|
21261
22318
|
}
|
|
21262
|
-
function
|
|
22319
|
+
function parsePositiveInt13(value, flag) {
|
|
21263
22320
|
const n = Number.parseInt(value, 10);
|
|
21264
22321
|
if (!Number.isInteger(n) || n <= 0) {
|
|
21265
22322
|
throw new Error(`Invalid ${flag}: ${value}`);
|
|
@@ -21269,7 +22326,7 @@ function parsePositiveInt12(value, flag) {
|
|
|
21269
22326
|
function formatAmount3(amountCents, currency) {
|
|
21270
22327
|
return `${(amountCents / 100).toFixed(2)} ${currency}`;
|
|
21271
22328
|
}
|
|
21272
|
-
function
|
|
22329
|
+
function requireConfirm8(confirm, what) {
|
|
21273
22330
|
if (!confirm) {
|
|
21274
22331
|
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
21275
22332
|
}
|
|
@@ -21290,7 +22347,7 @@ function registerOrdersCommands(program2) {
|
|
|
21290
22347
|
to: options.to,
|
|
21291
22348
|
search: options.search,
|
|
21292
22349
|
includeTest: options.includeTest,
|
|
21293
|
-
limit: options.limit !== void 0 ?
|
|
22350
|
+
limit: options.limit !== void 0 ? parsePositiveInt13(options.limit, "--limit") : void 0,
|
|
21294
22351
|
cursor: options.cursor
|
|
21295
22352
|
});
|
|
21296
22353
|
emit(flags, page, () => {
|
|
@@ -21368,7 +22425,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
21368
22425
|
).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(
|
|
21369
22426
|
async (orderId, options, command) => {
|
|
21370
22427
|
const flags = getGlobalFlags(command);
|
|
21371
|
-
|
|
22428
|
+
requireConfirm8(options.confirm, `approve order ${orderId} (access + purchase email)`);
|
|
21372
22429
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
21373
22430
|
const result = await client.approveManualOrder(orderId, {
|
|
21374
22431
|
issueInvoice: options.issueInvoice === true ? true : void 0
|
|
@@ -21385,7 +22442,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
21385
22442
|
"Reject a manual (wire-transfer) order \u2014 cancels it and notifies the buyer. 409 = the order was already decided. Requires --confirm"
|
|
21386
22443
|
).argument("<orderId>", "order id (a `pending` order from `orders list --status pending`)").option("--confirm", "required \u2014 acknowledge that the order is cancelled and the buyer emailed").action(async (orderId, options, command) => {
|
|
21387
22444
|
const flags = getGlobalFlags(command);
|
|
21388
|
-
|
|
22445
|
+
requireConfirm8(options.confirm, `reject order ${orderId} (cancel + notify the buyer)`);
|
|
21389
22446
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
21390
22447
|
const result = await client.rejectManualOrder(orderId);
|
|
21391
22448
|
emit(flags, result, () => {
|
|
@@ -21414,7 +22471,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
21414
22471
|
status: options.status !== void 0 ? assertOrderStatus(options.status) : void 0,
|
|
21415
22472
|
search: options.search,
|
|
21416
22473
|
includeTest: options.includeTest,
|
|
21417
|
-
maxRows: options.maxRows !== void 0 ?
|
|
22474
|
+
maxRows: options.maxRows !== void 0 ? parsePositiveInt13(options.maxRows, "--max-rows") : void 0
|
|
21418
22475
|
});
|
|
21419
22476
|
writeFileSync5(options.out, result.csv, "utf8");
|
|
21420
22477
|
emit(
|
|
@@ -21435,12 +22492,12 @@ Next page: --cursor ${page.nextCursor}
|
|
|
21435
22492
|
}
|
|
21436
22493
|
|
|
21437
22494
|
// src/commands/oto/index.ts
|
|
21438
|
-
import { readFileSync as
|
|
22495
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
21439
22496
|
var MIN_TIMER_MINUTES = 1;
|
|
21440
22497
|
var MAX_TIMER_MINUTES = 120;
|
|
21441
22498
|
function parseJsonFileOrInline7(value, flag) {
|
|
21442
22499
|
const trimmed = value.trim();
|
|
21443
|
-
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed :
|
|
22500
|
+
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync18(value, "utf8");
|
|
21444
22501
|
try {
|
|
21445
22502
|
return JSON.parse(raw);
|
|
21446
22503
|
} catch (error) {
|
|
@@ -21628,9 +22685,9 @@ function registerOtoCommands(program2) {
|
|
|
21628
22685
|
}
|
|
21629
22686
|
|
|
21630
22687
|
// src/commands/pages/index.ts
|
|
21631
|
-
import { existsSync as existsSync3, readFileSync as
|
|
22688
|
+
import { existsSync as existsSync3, readFileSync as readFileSync19 } from "fs";
|
|
21632
22689
|
import { readFile as readFile2 } from "fs/promises";
|
|
21633
|
-
async function
|
|
22690
|
+
async function resolveHtml2(options) {
|
|
21634
22691
|
if (options.file) {
|
|
21635
22692
|
return readFile2(options.file, "utf8");
|
|
21636
22693
|
}
|
|
@@ -21642,6 +22699,11 @@ var PAGE_INTENTS = [
|
|
|
21642
22699
|
"sales-page",
|
|
21643
22700
|
"thank-you",
|
|
21644
22701
|
"waitlist",
|
|
22702
|
+
"newsletter",
|
|
22703
|
+
"challenge",
|
|
22704
|
+
"application",
|
|
22705
|
+
"upsell",
|
|
22706
|
+
"low-ticket",
|
|
21645
22707
|
"generic"
|
|
21646
22708
|
];
|
|
21647
22709
|
var SOURCE_SITE_MODES = ["rebuild", "inspired"];
|
|
@@ -21655,7 +22717,7 @@ function parseChoice5(value, allowed, flag) {
|
|
|
21655
22717
|
}
|
|
21656
22718
|
return match;
|
|
21657
22719
|
}
|
|
21658
|
-
function
|
|
22720
|
+
function parsePositiveInt14(value, flag) {
|
|
21659
22721
|
const n = Number.parseInt(value, 10);
|
|
21660
22722
|
if (!Number.isInteger(n) || n <= 0) {
|
|
21661
22723
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
@@ -21670,13 +22732,13 @@ function parseCsv4(value) {
|
|
|
21670
22732
|
];
|
|
21671
22733
|
}
|
|
21672
22734
|
function resolveTextOrFile2(value) {
|
|
21673
|
-
const text = existsSync3(value) ?
|
|
22735
|
+
const text = existsSync3(value) ? readFileSync19(value, "utf8") : value;
|
|
21674
22736
|
if (!text.trim()) throw new Error("The text (or the file it points to) is empty.");
|
|
21675
22737
|
return text;
|
|
21676
22738
|
}
|
|
21677
22739
|
function parseJsonObjectFileOrInline3(value, flag) {
|
|
21678
22740
|
const trimmed = value.trim();
|
|
21679
|
-
const raw = trimmed.startsWith("{") ? trimmed :
|
|
22741
|
+
const raw = trimmed.startsWith("{") ? trimmed : readFileSync19(value, "utf8");
|
|
21680
22742
|
let parsed;
|
|
21681
22743
|
try {
|
|
21682
22744
|
parsed = JSON.parse(raw);
|
|
@@ -21688,7 +22750,7 @@ function parseJsonObjectFileOrInline3(value, flag) {
|
|
|
21688
22750
|
}
|
|
21689
22751
|
return parsed;
|
|
21690
22752
|
}
|
|
21691
|
-
function
|
|
22753
|
+
function requireConfirm9(confirm, what) {
|
|
21692
22754
|
if (!confirm) {
|
|
21693
22755
|
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
21694
22756
|
}
|
|
@@ -21725,7 +22787,7 @@ function buildGenerateInput(options) {
|
|
|
21725
22787
|
if (options.eventStart !== void 0) input.eventStartAt = options.eventStart;
|
|
21726
22788
|
if (options.eventTimezone !== void 0) input.eventTimezone = options.eventTimezone;
|
|
21727
22789
|
if (options.eventDuration !== void 0) {
|
|
21728
|
-
input.eventDurationMinutes =
|
|
22790
|
+
input.eventDurationMinutes = parsePositiveInt14(options.eventDuration, "--event-duration");
|
|
21729
22791
|
}
|
|
21730
22792
|
if (options.eventMode !== void 0) {
|
|
21731
22793
|
input.eventMode = parseChoice5(options.eventMode, EVENT_MODES2, "--event-mode");
|
|
@@ -21780,12 +22842,14 @@ function registerPagesCommands(program2) {
|
|
|
21780
22842
|
process.stdout.write(` Source: ${page.source}
|
|
21781
22843
|
`);
|
|
21782
22844
|
process.stdout.write(` HTML: ${page.html.length} bytes
|
|
22845
|
+
`);
|
|
22846
|
+
process.stdout.write(` Cookie banner: ${page.hideCookieBanner ? "hidden" : "shown"}
|
|
21783
22847
|
`);
|
|
21784
22848
|
});
|
|
21785
22849
|
});
|
|
21786
22850
|
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) => {
|
|
21787
22851
|
const flags = getGlobalFlags(command);
|
|
21788
|
-
const html = await
|
|
22852
|
+
const html = await resolveHtml2(options);
|
|
21789
22853
|
if (!html) {
|
|
21790
22854
|
throw new Error("Provide page HTML via --file <path> or --html <html>.");
|
|
21791
22855
|
}
|
|
@@ -21802,13 +22866,17 @@ function registerPagesCommands(program2) {
|
|
|
21802
22866
|
`);
|
|
21803
22867
|
});
|
|
21804
22868
|
});
|
|
21805
|
-
pages.command("update").description("Update a page (slug, title, or
|
|
22869
|
+
pages.command("update").description("Update a page (slug, title, HTML body, or cookie-banner visibility)").argument("<id>", "page id").option("--slug <slug>", "new slug").option("--title <title>", "new title").option("--file <path>", "path to a new .html file").option("--html <html>", "new inline HTML body").option("--cookie-banner", "show the workspace cookie banner on this page again").option(
|
|
22870
|
+
"--no-cookie-banner",
|
|
22871
|
+
"hide the workspace cookie banner on this page (pixels still wait for consent given elsewhere)"
|
|
22872
|
+
).action(async (id, options, command) => {
|
|
21806
22873
|
const flags = getGlobalFlags(command);
|
|
21807
|
-
const html = await
|
|
22874
|
+
const html = await resolveHtml2(options);
|
|
21808
22875
|
const input = {
|
|
21809
22876
|
slug: options.slug,
|
|
21810
22877
|
title: options.title,
|
|
21811
|
-
html
|
|
22878
|
+
html,
|
|
22879
|
+
...options.cookieBanner !== void 0 ? { hideCookieBanner: !options.cookieBanner } : {}
|
|
21812
22880
|
};
|
|
21813
22881
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
21814
22882
|
const result = await client.updatePage(id, input);
|
|
@@ -21877,7 +22945,7 @@ function registerPagesCommands(program2) {
|
|
|
21877
22945
|
"inline JSON object or .json path merged into the request body (explicit flags win)"
|
|
21878
22946
|
).option("--confirm", "required \u2014 acknowledge that AI credits will be consumed").action(async (options, command) => {
|
|
21879
22947
|
const flags = getGlobalFlags(command);
|
|
21880
|
-
|
|
22948
|
+
requireConfirm9(options.confirm, "generate a page (consumes AI credits)");
|
|
21881
22949
|
const input = buildGenerateInput(options);
|
|
21882
22950
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
21883
22951
|
const result = await client.generatePage(input).catch(withSchedulerHint3);
|
|
@@ -21899,7 +22967,7 @@ function registerPagesCommands(program2) {
|
|
|
21899
22967
|
).action(
|
|
21900
22968
|
async (pageId, options, command) => {
|
|
21901
22969
|
const flags = getGlobalFlags(command);
|
|
21902
|
-
|
|
22970
|
+
requireConfirm9(
|
|
21903
22971
|
options.confirm,
|
|
21904
22972
|
"AI-edit the page (consumes AI credits, replaces the HTML)"
|
|
21905
22973
|
);
|
|
@@ -21958,7 +23026,7 @@ function registerPagesCommands(program2) {
|
|
|
21958
23026
|
).argument("<pageId>", "page id").argument("<revisionId>", "revision id from `pages revisions`").option("--confirm", "required \u2014 acknowledge that the current HTML will be replaced").action(
|
|
21959
23027
|
async (pageId, revisionId, options, command) => {
|
|
21960
23028
|
const flags = getGlobalFlags(command);
|
|
21961
|
-
|
|
23029
|
+
requireConfirm9(options.confirm, "restore the revision (replaces the current HTML)");
|
|
21962
23030
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
21963
23031
|
const result = await client.restorePageRevision(pageId, revisionId);
|
|
21964
23032
|
emit(flags, result, () => {
|
|
@@ -21971,7 +23039,7 @@ function registerPagesCommands(program2) {
|
|
|
21971
23039
|
"One-click activation of everything generated around a page: publishes the DRAFT delivery automation and schedules the webinar follow-up broadcasts at their stored send times. Emails will go out for real; items whose copy still carries [FILL IN] placeholders are skipped, never sent. Requires --confirm."
|
|
21972
23040
|
).argument("<pageId>", "page id").option("--confirm", "required \u2014 acknowledge that automations go live and emails will send").action(async (pageId, options, command) => {
|
|
21973
23041
|
const flags = getGlobalFlags(command);
|
|
21974
|
-
|
|
23042
|
+
requireConfirm9(options.confirm, "activate the page automations (emails will send)");
|
|
21975
23043
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
21976
23044
|
const result = await client.activatePageAutomations(pageId);
|
|
21977
23045
|
emit(flags, result, () => {
|
|
@@ -22001,7 +23069,7 @@ function registerPagesCommands(program2) {
|
|
|
22001
23069
|
}
|
|
22002
23070
|
|
|
22003
23071
|
// src/commands/products/index.ts
|
|
22004
|
-
import { readFileSync as
|
|
23072
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
22005
23073
|
var KB_TREE_CHANGED_ERROR = "tree-changed";
|
|
22006
23074
|
var JSON_FLAG_HINT3 = "(write it as --json=<value> \u2014 a bare --json is the global JSON-output flag)";
|
|
22007
23075
|
function parseCoverRef(value) {
|
|
@@ -22107,7 +23175,7 @@ function assertOneOf3(value, allowed, flag) {
|
|
|
22107
23175
|
}
|
|
22108
23176
|
function parseJsonFileOrInline8(value, flag) {
|
|
22109
23177
|
const trimmed = value.trim();
|
|
22110
|
-
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed :
|
|
23178
|
+
const raw = trimmed.startsWith("{") || trimmed.startsWith("[") ? trimmed : readFileSync20(value, "utf8");
|
|
22111
23179
|
try {
|
|
22112
23180
|
return JSON.parse(raw);
|
|
22113
23181
|
} catch (error) {
|
|
@@ -22702,7 +23770,7 @@ function registerProductsCommands(program2) {
|
|
|
22702
23770
|
if (options.content !== void 0 && options.contentFile !== void 0) {
|
|
22703
23771
|
throw new Error("Use either --content or --content-file, not both.");
|
|
22704
23772
|
}
|
|
22705
|
-
const editorContent = options.contentFile !== void 0 ?
|
|
23773
|
+
const editorContent = options.contentFile !== void 0 ? readFileSync20(options.contentFile, "utf8") : options.content;
|
|
22706
23774
|
if (editorContent === void 0) {
|
|
22707
23775
|
throw new Error("Pass the description via --content or --content-file.");
|
|
22708
23776
|
}
|
|
@@ -22801,7 +23869,7 @@ function registerProductsCommands(program2) {
|
|
|
22801
23869
|
if (options.content !== void 0 && options.contentFile !== void 0) {
|
|
22802
23870
|
throw new Error("Use either --content or --content-file, not both.");
|
|
22803
23871
|
}
|
|
22804
|
-
return options.contentFile !== void 0 ?
|
|
23872
|
+
return options.contentFile !== void 0 ? readFileSync20(options.contentFile, "utf8") : options.content;
|
|
22805
23873
|
};
|
|
22806
23874
|
kb.command("get").description(
|
|
22807
23875
|
"Show the knowledge base: categories (tree keys = the --category ids) + all articles (drafts and unavailable included \u2014 the creator view)"
|
|
@@ -22966,15 +24034,25 @@ function registerProductsCommands(program2) {
|
|
|
22966
24034
|
}
|
|
22967
24035
|
);
|
|
22968
24036
|
const kbCategory = kb.command("category").description("Manage knowledge-base categories (add, rename, delete)");
|
|
22969
|
-
kbCategory.command("add").description(
|
|
22970
|
-
|
|
22971
|
-
|
|
22972
|
-
|
|
22973
|
-
|
|
22974
|
-
|
|
22975
|
-
|
|
22976
|
-
|
|
22977
|
-
|
|
24037
|
+
kbCategory.command("add").description(
|
|
24038
|
+
"Add a category to the knowledge base (a root category, or a subcategory with --parent)"
|
|
24039
|
+
).argument("<productId>", "knowledge-base product id").requiredOption("--name <name>", "category display name").option(
|
|
24040
|
+
"--parent <categoryId>",
|
|
24041
|
+
"nest the new category under this existing category (tree key from `products kb get`); nesting depth is unlimited"
|
|
24042
|
+
).action(
|
|
24043
|
+
async (productId, options, command) => {
|
|
24044
|
+
const flags = getGlobalFlags(command);
|
|
24045
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
24046
|
+
const result = await client.addKbCategory(productId, options.name, options.parent);
|
|
24047
|
+
emit(flags, result, () => {
|
|
24048
|
+
process.stdout.write(
|
|
24049
|
+
options.parent ? `Added subcategory ${result.categoryId} under ${options.parent} on product ${productId}.
|
|
24050
|
+
` : `Added category ${result.categoryId} on product ${productId}.
|
|
24051
|
+
`
|
|
24052
|
+
);
|
|
24053
|
+
});
|
|
24054
|
+
}
|
|
24055
|
+
);
|
|
22978
24056
|
kbCategory.command("rename").description("Rename a category (its articles' denormalized category name follows)").argument("<productId>", "knowledge-base product id").argument("<categoryId>", "category id (tree key from `products kb get`)").requiredOption("--name <name>", "the new category name").action(
|
|
22979
24057
|
async (productId, categoryId, options, command) => {
|
|
22980
24058
|
const flags = getGlobalFlags(command);
|
|
@@ -23011,7 +24089,7 @@ function registerProductsCommands(program2) {
|
|
|
23011
24089
|
}
|
|
23012
24090
|
|
|
23013
24091
|
// src/commands/referrals/index.ts
|
|
23014
|
-
import { readFileSync as
|
|
24092
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
23015
24093
|
var PROGRAM_KINDS = ["referral", "affiliate"];
|
|
23016
24094
|
var WRITABLE_STATUSES = ["active", "inactive"];
|
|
23017
24095
|
var ENROLLMENT_MODES = ["open", "inviteOnly", "closed"];
|
|
@@ -23036,7 +24114,7 @@ function parseIdList3(value, flag) {
|
|
|
23036
24114
|
}
|
|
23037
24115
|
function parseJsonObjectFileOrInline4(value, flag) {
|
|
23038
24116
|
const trimmed = value.trim();
|
|
23039
|
-
const raw = trimmed.startsWith("{") ? trimmed :
|
|
24117
|
+
const raw = trimmed.startsWith("{") ? trimmed : readFileSync21(value, "utf8");
|
|
23040
24118
|
let parsed;
|
|
23041
24119
|
try {
|
|
23042
24120
|
parsed = JSON.parse(raw);
|
|
@@ -23074,7 +24152,7 @@ function parseDeepConfig(value) {
|
|
|
23074
24152
|
}
|
|
23075
24153
|
return parsed;
|
|
23076
24154
|
}
|
|
23077
|
-
function
|
|
24155
|
+
function requireConfirm10(confirm, what) {
|
|
23078
24156
|
if (!confirm) {
|
|
23079
24157
|
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
23080
24158
|
}
|
|
@@ -23309,7 +24387,7 @@ Failed deliveries (${result.failedDeliveries.length})
|
|
|
23309
24387
|
"Archive a program \u2014 new referrals stop counting and it leaves the active list. Requires --confirm."
|
|
23310
24388
|
).argument("<programId>", "program id").option("--confirm", "required \u2014 acknowledge the archive").action(async (programId, options, command) => {
|
|
23311
24389
|
const flags = getGlobalFlags(command);
|
|
23312
|
-
|
|
24390
|
+
requireConfirm10(options.confirm, `archive program ${programId}`);
|
|
23313
24391
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
23314
24392
|
const result = await client.archiveReferralProgram(programId);
|
|
23315
24393
|
emit(flags, result, () => {
|
|
@@ -23341,7 +24419,7 @@ Failed deliveries (${result.failedDeliveries.length})
|
|
|
23341
24419
|
"Enroll EVERY client with access to one of the program products. Requires --confirm. Reports enrolled / alreadyEnrolled / skippedWithoutAccount counts."
|
|
23342
24420
|
).argument("<programId>", "program id").option("--confirm", "required \u2014 acknowledge the bulk enrollment").action(async (programId, options, command) => {
|
|
23343
24421
|
const flags = getGlobalFlags(command);
|
|
23344
|
-
|
|
24422
|
+
requireConfirm10(options.confirm, `enroll all clients in program ${programId}`);
|
|
23345
24423
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
23346
24424
|
const result = await client.enrollAllReferralClients(programId);
|
|
23347
24425
|
emit(flags, result, () => {
|
|
@@ -23357,7 +24435,7 @@ Failed deliveries (${result.failedDeliveries.length})
|
|
|
23357
24435
|
async (programId, entryId, options, command) => {
|
|
23358
24436
|
const flags = getGlobalFlags(command);
|
|
23359
24437
|
const decision = parseChoice6(options.decision, REWARD_DECISIONS, "--decision");
|
|
23360
|
-
|
|
24438
|
+
requireConfirm10(options.confirm, `${decision} reward ${entryId} in program ${programId}`);
|
|
23361
24439
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
23362
24440
|
const result = await client.resolveReferralReward(programId, entryId, { decision });
|
|
23363
24441
|
emit(flags, result, () => {
|
|
@@ -23387,8 +24465,8 @@ Failed deliveries (${result.failedDeliveries.length})
|
|
|
23387
24465
|
}
|
|
23388
24466
|
|
|
23389
24467
|
// src/commands/segments/index.ts
|
|
23390
|
-
import { readFileSync as
|
|
23391
|
-
var
|
|
24468
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
24469
|
+
var SUBSCRIPTION_STATUSES2 = ["pending", "subscribed", "unsubscribed"];
|
|
23392
24470
|
function collectRepeatable6(value, previous) {
|
|
23393
24471
|
return [...previous, value];
|
|
23394
24472
|
}
|
|
@@ -23399,14 +24477,14 @@ function parseMinorUnits(value, flag) {
|
|
|
23399
24477
|
}
|
|
23400
24478
|
return n;
|
|
23401
24479
|
}
|
|
23402
|
-
var CONDITION_FLAGS_HELP = "--has-tag/--without-tag, --owns/--not-owns, --spent-gte/--spent-lte, --subscription, or --json-file";
|
|
24480
|
+
var CONDITION_FLAGS_HELP = "--has-tag/--without-tag, --owns/--not-owns, --owns-direct/--not-owns-direct, --spent-gte/--spent-lte, --subscription, or --json-file";
|
|
23403
24481
|
function resolveConditions(options) {
|
|
23404
|
-
const hasFlagConditions = options.hasTag.length > 0 || options.withoutTag.length > 0 || options.owns.length > 0 || options.notOwns.length > 0 || options.spentGte !== void 0 || options.spentLte !== void 0 || options.subscription !== void 0;
|
|
24482
|
+
const hasFlagConditions = options.hasTag.length > 0 || options.withoutTag.length > 0 || options.owns.length > 0 || options.notOwns.length > 0 || options.ownsDirect.length > 0 || options.notOwnsDirect.length > 0 || options.spentGte !== void 0 || options.spentLte !== void 0 || options.subscription !== void 0;
|
|
23405
24483
|
if (options.jsonFile !== void 0) {
|
|
23406
24484
|
if (hasFlagConditions) {
|
|
23407
24485
|
throw new Error("Use either condition flags or --json-file, not both.");
|
|
23408
24486
|
}
|
|
23409
|
-
const raw =
|
|
24487
|
+
const raw = readFileSync22(options.jsonFile, "utf8");
|
|
23410
24488
|
let parsed;
|
|
23411
24489
|
try {
|
|
23412
24490
|
parsed = JSON.parse(raw);
|
|
@@ -23434,6 +24512,12 @@ function resolveConditions(options) {
|
|
|
23434
24512
|
for (const productId of options.notOwns) {
|
|
23435
24513
|
conditions.push({ field: "product", operator: "not-owns", productId });
|
|
23436
24514
|
}
|
|
24515
|
+
for (const productId of options.ownsDirect) {
|
|
24516
|
+
conditions.push({ field: "product", operator: "owns", productId, accessScope: "direct" });
|
|
24517
|
+
}
|
|
24518
|
+
for (const productId of options.notOwnsDirect) {
|
|
24519
|
+
conditions.push({ field: "product", operator: "not-owns", productId, accessScope: "direct" });
|
|
24520
|
+
}
|
|
23437
24521
|
if (options.spentGte !== void 0) {
|
|
23438
24522
|
conditions.push({
|
|
23439
24523
|
field: "totalSpent",
|
|
@@ -23449,10 +24533,10 @@ function resolveConditions(options) {
|
|
|
23449
24533
|
});
|
|
23450
24534
|
}
|
|
23451
24535
|
if (options.subscription !== void 0) {
|
|
23452
|
-
const status =
|
|
24536
|
+
const status = SUBSCRIPTION_STATUSES2.find((s) => s === options.subscription);
|
|
23453
24537
|
if (!status) {
|
|
23454
24538
|
throw new Error(
|
|
23455
|
-
`Invalid --subscription: ${options.subscription} (expected one of ${
|
|
24539
|
+
`Invalid --subscription: ${options.subscription} (expected one of ${SUBSCRIPTION_STATUSES2.join(", ")})`
|
|
23456
24540
|
);
|
|
23457
24541
|
}
|
|
23458
24542
|
conditions.push({ field: "emailSubscription", operator: "is", status });
|
|
@@ -23472,12 +24556,22 @@ function withConditionOptions(command) {
|
|
|
23472
24556
|
[]
|
|
23473
24557
|
).option(
|
|
23474
24558
|
"--owns <productId>",
|
|
23475
|
-
"client has
|
|
24559
|
+
"client has ACTIVE access to this product - a direct grant OR through a bundle (repeatable)",
|
|
23476
24560
|
collectRepeatable6,
|
|
23477
24561
|
[]
|
|
23478
24562
|
).option(
|
|
23479
24563
|
"--not-owns <productId>",
|
|
23480
|
-
"client has NO active access
|
|
24564
|
+
"client has NO active access to this product, direct or through a bundle (repeatable)",
|
|
24565
|
+
collectRepeatable6,
|
|
24566
|
+
[]
|
|
24567
|
+
).option(
|
|
24568
|
+
"--owns-direct <productId>",
|
|
24569
|
+
"client has an ACTIVE grant on this product itself (bought / granted by hand) - bundle access does not count; use to mail only the buyers of that one product (repeatable)",
|
|
24570
|
+
collectRepeatable6,
|
|
24571
|
+
[]
|
|
24572
|
+
).option(
|
|
24573
|
+
"--not-owns-direct <productId>",
|
|
24574
|
+
"client has NO active direct grant on this product - bundle holders DO match (repeatable)",
|
|
23481
24575
|
collectRepeatable6,
|
|
23482
24576
|
[]
|
|
23483
24577
|
).option(
|
|
@@ -23485,7 +24579,7 @@ function withConditionOptions(command) {
|
|
|
23485
24579
|
"lifetime spend at least this (minor units, e.g. 10000 = 100.00)"
|
|
23486
24580
|
).option("--spent-lte <minorUnits>", "lifetime spend at most this (minor units)").option(
|
|
23487
24581
|
"--subscription <status>",
|
|
23488
|
-
`marketing-email subscription state is (${
|
|
24582
|
+
`marketing-email subscription state is (${SUBSCRIPTION_STATUSES2.join(" | ")})`
|
|
23489
24583
|
).option(
|
|
23490
24584
|
"--json-file <path>",
|
|
23491
24585
|
"read the conditions as a JSON array from a file instead of flags"
|
|
@@ -23496,7 +24590,7 @@ function describeCondition(condition) {
|
|
|
23496
24590
|
case "tag":
|
|
23497
24591
|
return `${condition.operator === "has" ? "has" : "lacks"} tag ${condition.tagId}`;
|
|
23498
24592
|
case "product":
|
|
23499
|
-
return `${condition.operator === "owns" ? "owns" : "does not own"} product ${condition.productId}`;
|
|
24593
|
+
return `${condition.operator === "owns" ? "owns" : "does not own"} product ${condition.productId}${condition.accessScope === "direct" ? " (direct)" : ""}`;
|
|
23500
24594
|
case "totalSpent":
|
|
23501
24595
|
return `total spent ${condition.operator === "gte" ? ">=" : "<="} ${condition.amount} (minor units)`;
|
|
23502
24596
|
case "emailSubscription":
|
|
@@ -23505,6 +24599,10 @@ function describeCondition(condition) {
|
|
|
23505
24599
|
return `last email engagement ${condition.operator === "within" ? "within" : "not within"} the last ${condition.days} days`;
|
|
23506
24600
|
case "lastOrderAt":
|
|
23507
24601
|
return `last paid order ${condition.operator === "within" ? "within" : "not within"} the last ${condition.days} days`;
|
|
24602
|
+
case "smsSubscription":
|
|
24603
|
+
return `sms consent is ${condition.status}`;
|
|
24604
|
+
case "phoneNumber":
|
|
24605
|
+
return condition.operator === "has" ? "has a phone number" : "has no phone number";
|
|
23508
24606
|
}
|
|
23509
24607
|
}
|
|
23510
24608
|
function registerSegmentsCommands(program2) {
|
|
@@ -23602,7 +24700,7 @@ function registerSegmentsCommands(program2) {
|
|
|
23602
24700
|
}
|
|
23603
24701
|
|
|
23604
24702
|
// src/commands/site/index.ts
|
|
23605
|
-
import { existsSync as existsSync4, readFileSync as
|
|
24703
|
+
import { existsSync as existsSync4, readFileSync as readFileSync23 } from "fs";
|
|
23606
24704
|
var MAIN_PAGE_MODES = {
|
|
23607
24705
|
sales: "/",
|
|
23608
24706
|
dashboard: "/dashboard",
|
|
@@ -23626,14 +24724,14 @@ var HTTP_CONFLICT = 409;
|
|
|
23626
24724
|
var HTTP_BAD_REQUEST = 400;
|
|
23627
24725
|
var HTTP_NOT_FOUND = 404;
|
|
23628
24726
|
var HTTP_FORBIDDEN = 403;
|
|
23629
|
-
function
|
|
24727
|
+
function requireConfirm11(confirm, what) {
|
|
23630
24728
|
if (!confirm) {
|
|
23631
24729
|
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
23632
24730
|
}
|
|
23633
24731
|
}
|
|
23634
24732
|
function parseJsonObjectFileOrInline5(value, flag) {
|
|
23635
24733
|
const trimmed = value.trim();
|
|
23636
|
-
const raw = trimmed.startsWith("{") ? trimmed :
|
|
24734
|
+
const raw = trimmed.startsWith("{") ? trimmed : readFileSync23(value, "utf8");
|
|
23637
24735
|
let parsed;
|
|
23638
24736
|
try {
|
|
23639
24737
|
parsed = JSON.parse(raw);
|
|
@@ -23685,7 +24783,7 @@ function parseDomainList(value, what) {
|
|
|
23685
24783
|
return items;
|
|
23686
24784
|
}
|
|
23687
24785
|
function readMarkdownOrFile(value) {
|
|
23688
|
-
return existsSync4(value) ?
|
|
24786
|
+
return existsSync4(value) ? readFileSync23(value, "utf8") : value;
|
|
23689
24787
|
}
|
|
23690
24788
|
function assertNonEmptyBody(body, hint) {
|
|
23691
24789
|
if (Object.keys(body).length === 0) {
|
|
@@ -24168,7 +25266,7 @@ function registerSiteCommands(program2) {
|
|
|
24168
25266
|
throw new Error("Pass exactly one of --on or --off.");
|
|
24169
25267
|
}
|
|
24170
25268
|
const active = Boolean(options.on);
|
|
24171
|
-
|
|
25269
|
+
requireConfirm11(options.confirm, active ? "publish the site" : "take the site offline");
|
|
24172
25270
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
24173
25271
|
const result = await client.activateSite({ active });
|
|
24174
25272
|
emit(
|
|
@@ -24183,7 +25281,7 @@ function registerSiteCommands(program2) {
|
|
|
24183
25281
|
"Change the slug \u2014 heavy: re-registers Stripe/Firebase/Vercel hosts and purges caches. 409 = slug taken. Requires --confirm"
|
|
24184
25282
|
).argument("<slug>", 'new slug (lower-case letters, digits, dashes; "www" is refused)').option("--confirm", "required \u2014 acknowledge the URL change for every existing link").action(async (value, options, command) => {
|
|
24185
25283
|
const flags = getGlobalFlags(command);
|
|
24186
|
-
|
|
25284
|
+
requireConfirm11(options.confirm, `change the site slug to "${value}"`);
|
|
24187
25285
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
24188
25286
|
const result = await client.updateSiteSlug({ slug: value.trim() }).catch(withSlugHint);
|
|
24189
25287
|
emit(flags, result, () => process.stdout.write(`Slug changed to "${value.trim()}".
|
|
@@ -24195,7 +25293,7 @@ function registerSiteCommands(program2) {
|
|
|
24195
25293
|
).argument("<domains>", "comma-separated hostnames, e.g. shop.example.com,example.com \u2014 or none").option("--confirm", "required \u2014 acknowledge the domain list replace").action(async (value, options, command) => {
|
|
24196
25294
|
const flags = getGlobalFlags(command);
|
|
24197
25295
|
const list = value.trim().toLowerCase() === "none" ? [] : parseDomainList(value, "<domains>");
|
|
24198
|
-
|
|
25296
|
+
requireConfirm11(
|
|
24199
25297
|
options.confirm,
|
|
24200
25298
|
list.length === 0 ? "remove every custom domain" : `set the custom domains to ${list.join(", ")}`
|
|
24201
25299
|
);
|
|
@@ -24280,7 +25378,7 @@ function registerSiteCommands(program2) {
|
|
|
24280
25378
|
"Nothing to update \u2014 pass --terms-content|--terms-url and/or --privacy-content|--privacy-url."
|
|
24281
25379
|
);
|
|
24282
25380
|
}
|
|
24283
|
-
|
|
25381
|
+
requireConfirm11(options.confirm, "change the legal documents");
|
|
24284
25382
|
const body = {};
|
|
24285
25383
|
if (options.language !== void 0) body.language = options.language;
|
|
24286
25384
|
if (terms) body.terms = terms;
|
|
@@ -24331,27 +25429,323 @@ function printStorefront(config) {
|
|
|
24331
25429
|
);
|
|
24332
25430
|
}
|
|
24333
25431
|
|
|
24334
|
-
// src/commands/
|
|
24335
|
-
|
|
24336
|
-
|
|
24337
|
-
|
|
24338
|
-
|
|
25432
|
+
// src/commands/smsCampaigns/index.ts
|
|
25433
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
25434
|
+
var SMS_PROVIDERS = ["twilio", "smsapi"];
|
|
25435
|
+
var RECIPIENT_STATUSES2 = [
|
|
25436
|
+
"pending",
|
|
25437
|
+
"sent",
|
|
25438
|
+
"failed",
|
|
25439
|
+
"skipped-invalid-number"
|
|
25440
|
+
];
|
|
25441
|
+
var SCHEDULER_UNAVAILABLE_STATUS4 = 503;
|
|
25442
|
+
function parsePositiveInt15(value, flag) {
|
|
25443
|
+
const n = Number.parseInt(value, 10);
|
|
25444
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
25445
|
+
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
24339
25446
|
}
|
|
24340
|
-
|
|
24341
|
-
|
|
24342
|
-
|
|
25447
|
+
return n;
|
|
25448
|
+
}
|
|
25449
|
+
function requireConfirm12(confirm, refusal) {
|
|
25450
|
+
if (!confirm) throw new Error(refusal);
|
|
25451
|
+
}
|
|
25452
|
+
function withSchedulerHint4(error) {
|
|
25453
|
+
if (error instanceof ApiError && error.status === SCHEDULER_UNAVAILABLE_STATUS4) {
|
|
25454
|
+
throw new ApiError(
|
|
25455
|
+
error.status,
|
|
25456
|
+
"Trigger.dev scheduler not configured \u2014 SMS campaigns cannot be sent on this environment.",
|
|
25457
|
+
"The API needs TRIGGER_SECRET_KEY (Trigger.dev) set; ask the operator to configure it."
|
|
25458
|
+
);
|
|
24343
25459
|
}
|
|
24344
|
-
|
|
24345
|
-
return {
|
|
24346
|
-
text: options.text,
|
|
24347
|
-
targets,
|
|
24348
|
-
...mediaIds.length > 0 ? { mediaIds } : {},
|
|
24349
|
-
...options.firstComment ? { firstComment: options.firstComment } : {},
|
|
24350
|
-
...options.at ? { scheduledAt: options.at } : {},
|
|
24351
|
-
...options.publish ? { publish: true } : {}
|
|
24352
|
-
};
|
|
25460
|
+
throw error;
|
|
24353
25461
|
}
|
|
24354
|
-
function
|
|
25462
|
+
function resolveBody(options) {
|
|
25463
|
+
if (options.body !== void 0 && options.bodyFile !== void 0) {
|
|
25464
|
+
throw new Error("Use either --body or --body-file, not both.");
|
|
25465
|
+
}
|
|
25466
|
+
return options.bodyFile !== void 0 ? readFileSync24(options.bodyFile, "utf8") : options.body;
|
|
25467
|
+
}
|
|
25468
|
+
function resolveAudience2(options) {
|
|
25469
|
+
if (options.segment !== void 0 && options.allSubscribed) {
|
|
25470
|
+
throw new Error("Use either --segment or --all-subscribed, not both.");
|
|
25471
|
+
}
|
|
25472
|
+
if (options.segment !== void 0) return { type: "segment", segmentId: options.segment };
|
|
25473
|
+
if (options.allSubscribed) return { type: "all-sms-subscribed" };
|
|
25474
|
+
return void 0;
|
|
25475
|
+
}
|
|
25476
|
+
function describeAudience2(audience) {
|
|
25477
|
+
return audience.type === "segment" ? `segment ${audience.segmentId} (SMS-subscribed clients only)` : "all SMS-subscribed clients";
|
|
25478
|
+
}
|
|
25479
|
+
function resolveProvider(options) {
|
|
25480
|
+
if (options.provider !== void 0 && options.defaultProvider) {
|
|
25481
|
+
throw new Error("Use either --provider or --default-provider, not both.");
|
|
25482
|
+
}
|
|
25483
|
+
if (options.defaultProvider) return null;
|
|
25484
|
+
if (options.provider === void 0) return void 0;
|
|
25485
|
+
const match = SMS_PROVIDERS.find((p) => p === options.provider);
|
|
25486
|
+
if (!match) {
|
|
25487
|
+
throw new Error(
|
|
25488
|
+
`Invalid --provider: ${options.provider} (expected one of ${SMS_PROVIDERS.join(", ")})`
|
|
25489
|
+
);
|
|
25490
|
+
}
|
|
25491
|
+
return match;
|
|
25492
|
+
}
|
|
25493
|
+
function resolveSender3(options) {
|
|
25494
|
+
if (options.sender !== void 0 && options.defaultSender) {
|
|
25495
|
+
throw new Error("Use either --sender or --default-sender, not both.");
|
|
25496
|
+
}
|
|
25497
|
+
return options.defaultSender ? null : options.sender;
|
|
25498
|
+
}
|
|
25499
|
+
function resolveOptOutHint(options) {
|
|
25500
|
+
if (options.optOutHint !== void 0 && options.clearOptOutHint) {
|
|
25501
|
+
throw new Error("Use either --opt-out-hint or --clear-opt-out-hint, not both.");
|
|
25502
|
+
}
|
|
25503
|
+
return options.clearOptOutHint ? null : options.optOutHint;
|
|
25504
|
+
}
|
|
25505
|
+
function printCampaignDetail2(campaign) {
|
|
25506
|
+
process.stdout.write(`SMS campaign ${campaign.id}
|
|
25507
|
+
`);
|
|
25508
|
+
process.stdout.write(` Name: ${campaign.name}
|
|
25509
|
+
`);
|
|
25510
|
+
process.stdout.write(` Status: ${campaign.status}
|
|
25511
|
+
`);
|
|
25512
|
+
process.stdout.write(` Provider: ${campaign.provider ?? "(workspace default)"}
|
|
25513
|
+
`);
|
|
25514
|
+
process.stdout.write(` Sender: ${campaign.sender ?? "(connection's default)"}
|
|
25515
|
+
`);
|
|
25516
|
+
process.stdout.write(` Audience: ${describeAudience2(campaign.audience)}
|
|
25517
|
+
`);
|
|
25518
|
+
process.stdout.write(` Recipients: ${campaign.recipientCount ?? "(not resolved yet)"}
|
|
25519
|
+
`);
|
|
25520
|
+
if (campaign.scheduledAt) process.stdout.write(` Scheduled at: ${campaign.scheduledAt}
|
|
25521
|
+
`);
|
|
25522
|
+
if (campaign.sendStartedAt) {
|
|
25523
|
+
process.stdout.write(` Send started at: ${campaign.sendStartedAt}
|
|
25524
|
+
`);
|
|
25525
|
+
}
|
|
25526
|
+
if (campaign.completedAt) process.stdout.write(` Completed at: ${campaign.completedAt}
|
|
25527
|
+
`);
|
|
25528
|
+
if (campaign.failureReason) process.stdout.write(` Failure: ${campaign.failureReason}
|
|
25529
|
+
`);
|
|
25530
|
+
const { stats } = campaign;
|
|
25531
|
+
process.stdout.write(
|
|
25532
|
+
` Stats: sent ${stats.sent}, failed ${stats.failed}, skipped (invalid number) ${stats.skippedInvalidNumber}, billed parts ${stats.segments}
|
|
25533
|
+
`
|
|
25534
|
+
);
|
|
25535
|
+
process.stdout.write(` Opt-out line: ${campaign.optOutHint ?? "(none)"}
|
|
25536
|
+
`);
|
|
25537
|
+
process.stdout.write(` Body (${campaign.body.length} chars):
|
|
25538
|
+
`);
|
|
25539
|
+
for (const line of campaign.body.split("\n")) process.stdout.write(` ${line}
|
|
25540
|
+
`);
|
|
25541
|
+
}
|
|
25542
|
+
function registerSmsCampaignsCommands(program2) {
|
|
25543
|
+
const smsCampaigns = program2.command("sms-campaigns").description(
|
|
25544
|
+
"Manage SMS campaigns (one-off text broadcasts through the workspace's own Twilio / SMSAPI account) \u2014 drafts are harmless; `send` texts people and costs money"
|
|
25545
|
+
);
|
|
25546
|
+
smsCampaigns.command("list").description("List SMS campaigns").action(async (_, command) => {
|
|
25547
|
+
const flags = getGlobalFlags(command);
|
|
25548
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25549
|
+
const page = await client.listSmsCampaigns();
|
|
25550
|
+
emit(flags, page, () => {
|
|
25551
|
+
printTable(
|
|
25552
|
+
page.campaigns.map((c) => ({
|
|
25553
|
+
id: c.id,
|
|
25554
|
+
name: c.name,
|
|
25555
|
+
status: c.status,
|
|
25556
|
+
audience: describeAudience2(c.audience),
|
|
25557
|
+
recipients: c.recipientCount ?? "",
|
|
25558
|
+
sent: c.stats.sent,
|
|
25559
|
+
parts: c.stats.segments
|
|
25560
|
+
})),
|
|
25561
|
+
["id", "name", "status", "audience", "recipients", "sent", "parts"]
|
|
25562
|
+
);
|
|
25563
|
+
});
|
|
25564
|
+
});
|
|
25565
|
+
smsCampaigns.command("get").description("Show a single SMS campaign (status, provider, sender, audience, stats, body)").argument("<id>", "SMS campaign id").action(async (id, _options, command) => {
|
|
25566
|
+
const flags = getGlobalFlags(command);
|
|
25567
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25568
|
+
const result = await client.getSmsCampaign(id);
|
|
25569
|
+
emit(flags, result, () => printCampaignDetail2(result.campaign));
|
|
25570
|
+
});
|
|
25571
|
+
smsCampaigns.command("create").description(
|
|
25572
|
+
"Create a DRAFT SMS campaign \u2014 nothing is sent; only --name is required at draft time (the body and a connected provider are enforced by `sms-campaigns send`)"
|
|
25573
|
+
).requiredOption("--name <name>", "internal label shown in the campaigns list").option("--body <text>", "message text; {{firstName}} / {{lastName}} tokens are supported").option("--body-file <path>", "read the message text from a file").option("--opt-out-hint <text>", "opt-out line appended to every message (counts as text)").option("--provider <provider>", `send through this provider (${SMS_PROVIDERS.join(" | ")})`).option("--sender <name>", "sender name override (SMSAPI); omit for the connection's default").option("--segment <segmentId>", "send to a saved segment (SMS-subscribed clients in it only)").option("--all-subscribed", "send to every SMS-subscribed client (the default audience)").action(
|
|
25574
|
+
async (options, command) => {
|
|
25575
|
+
const flags = getGlobalFlags(command);
|
|
25576
|
+
const body = resolveBody(options);
|
|
25577
|
+
const audience = resolveAudience2(options);
|
|
25578
|
+
const provider = resolveProvider(options);
|
|
25579
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25580
|
+
const result = await client.createSmsCampaign({
|
|
25581
|
+
name: options.name,
|
|
25582
|
+
body,
|
|
25583
|
+
optOutHint: options.optOutHint,
|
|
25584
|
+
provider,
|
|
25585
|
+
sender: options.sender,
|
|
25586
|
+
audience
|
|
25587
|
+
});
|
|
25588
|
+
emit(flags, result, () => {
|
|
25589
|
+
process.stdout.write(
|
|
25590
|
+
`Created draft SMS campaign ${result.campaign.id} (${result.campaign.name}) \u2014 audience: ${describeAudience2(result.campaign.audience)}. Nothing is sent until \`sms-campaigns send\`.
|
|
25591
|
+
`
|
|
25592
|
+
);
|
|
25593
|
+
});
|
|
25594
|
+
}
|
|
25595
|
+
);
|
|
25596
|
+
smsCampaigns.command("update").description("Update a DRAFT SMS campaign (campaigns past draft are immutable except cancel)").argument("<id>", "SMS campaign id").option("--name <name>", "new internal label").option("--body <text>", "new message text (replaces the whole body)").option("--body-file <path>", "read the new message text from a file").option("--opt-out-hint <text>", "new opt-out line").option("--clear-opt-out-hint", "remove the opt-out line").option("--provider <provider>", `send through this provider (${SMS_PROVIDERS.join(" | ")})`).option("--default-provider", "go back to the workspace default provider").option("--sender <name>", "sender name override (SMSAPI)").option("--default-sender", "go back to the connection's default sender").option("--segment <segmentId>", "switch the audience to a saved segment").option("--all-subscribed", "switch the audience to every SMS-subscribed client").action(
|
|
25597
|
+
async (id, options, command) => {
|
|
25598
|
+
const flags = getGlobalFlags(command);
|
|
25599
|
+
const input = {};
|
|
25600
|
+
if (options.name !== void 0) input.name = options.name;
|
|
25601
|
+
const body = resolveBody(options);
|
|
25602
|
+
if (body !== void 0) input.body = body;
|
|
25603
|
+
const optOutHint = resolveOptOutHint(options);
|
|
25604
|
+
if (optOutHint !== void 0) input.optOutHint = optOutHint;
|
|
25605
|
+
const provider = resolveProvider(options);
|
|
25606
|
+
if (provider !== void 0) input.provider = provider;
|
|
25607
|
+
const sender = resolveSender3(options);
|
|
25608
|
+
if (sender !== void 0) input.sender = sender;
|
|
25609
|
+
const audience = resolveAudience2(options);
|
|
25610
|
+
if (audience !== void 0) input.audience = audience;
|
|
25611
|
+
if (Object.keys(input).length === 0) {
|
|
25612
|
+
throw new Error(
|
|
25613
|
+
"Nothing to update \u2014 pass at least one change flag (see `sms-campaigns update --help`)."
|
|
25614
|
+
);
|
|
25615
|
+
}
|
|
25616
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25617
|
+
const result = await client.updateSmsCampaign(id, input);
|
|
25618
|
+
emit(flags, result, () => {
|
|
25619
|
+
process.stdout.write(`Updated SMS campaign ${result.campaign.id} (still a draft).
|
|
25620
|
+
`);
|
|
25621
|
+
});
|
|
25622
|
+
}
|
|
25623
|
+
);
|
|
25624
|
+
smsCampaigns.command("delete").description(
|
|
25625
|
+
"PERMANENTLY delete an SMS campaign \u2014 cannot be undone; draft/cancelled/failed only (sent campaigns are audit records and stay). Requires --confirm"
|
|
25626
|
+
).argument("<id>", "SMS campaign id").option("--confirm", "required \u2014 acknowledge the permanent deletion").action(async (id, options, command) => {
|
|
25627
|
+
const flags = getGlobalFlags(command);
|
|
25628
|
+
requireConfirm12(
|
|
25629
|
+
options.confirm,
|
|
25630
|
+
`Refusing to delete SMS campaign ${id} without --confirm. Re-run with --confirm to proceed.`
|
|
25631
|
+
);
|
|
25632
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25633
|
+
const result = await client.deleteSmsCampaign(id);
|
|
25634
|
+
emit(flags, result, () => {
|
|
25635
|
+
process.stdout.write(`Deleted SMS campaign ${id}.
|
|
25636
|
+
`);
|
|
25637
|
+
});
|
|
25638
|
+
});
|
|
25639
|
+
smsCampaigns.command("send").description(
|
|
25640
|
+
"\u26A0\uFE0F SENDS the campaign \u2014 REAL text messages to the entire resolved audience through the workspace's own SMS provider account: it COSTS MONEY per message part and is IRREVERSIBLE. `sms-campaigns cancel` only stops remaining sends. Requires --confirm. Omit --at to send NOW; --at schedules (max 30 days out)."
|
|
25641
|
+
).argument("<id>", "SMS campaign id").option(
|
|
25642
|
+
"--at <isoTimestamp>",
|
|
25643
|
+
"schedule the send (ISO, max 30 days out) instead of sending now"
|
|
25644
|
+
).option("--confirm", "required \u2014 acknowledge this texts the audience for real").action(async (id, options, command) => {
|
|
25645
|
+
const flags = getGlobalFlags(command);
|
|
25646
|
+
requireConfirm12(
|
|
25647
|
+
options.confirm,
|
|
25648
|
+
`Refusing to send without --confirm. \`sms-campaigns send\` texts the ENTIRE resolved audience for real through the workspace's SMS provider account, is billed per message part and cannot be undone. Re-run with --confirm to proceed (check the draft first with \`sms-campaigns get ${id}\`).`
|
|
25649
|
+
);
|
|
25650
|
+
if (options.at !== void 0 && Number.isNaN(Date.parse(options.at))) {
|
|
25651
|
+
throw new Error(
|
|
25652
|
+
`Invalid --at timestamp: ${options.at} (expected ISO, e.g. 2026-10-15T09:00:00Z)`
|
|
25653
|
+
);
|
|
25654
|
+
}
|
|
25655
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25656
|
+
const result = await client.sendSmsCampaign(id, options.at).catch(withSchedulerHint4);
|
|
25657
|
+
emit(flags, result, () => {
|
|
25658
|
+
const { campaign } = result;
|
|
25659
|
+
if (campaign.status === "scheduled") {
|
|
25660
|
+
process.stdout.write(
|
|
25661
|
+
`SMS campaign ${campaign.id} scheduled for ${campaign.scheduledAt} \u2014 stop it before then with \`sms-campaigns cancel\`.
|
|
25662
|
+
`
|
|
25663
|
+
);
|
|
25664
|
+
} else {
|
|
25665
|
+
process.stdout.write(
|
|
25666
|
+
`SMS campaign ${campaign.id} is sending now (status: ${campaign.status}). \`sms-campaigns cancel\` can only stop the remaining sends.
|
|
25667
|
+
`
|
|
25668
|
+
);
|
|
25669
|
+
}
|
|
25670
|
+
});
|
|
25671
|
+
});
|
|
25672
|
+
smsCampaigns.command("cancel").description(
|
|
25673
|
+
"Cancel a scheduled or in-flight SMS campaign \u2014 stops REMAINING sends only; messages already handed to the provider are out"
|
|
25674
|
+
).argument("<id>", "SMS campaign id").action(async (id, _options, command) => {
|
|
25675
|
+
const flags = getGlobalFlags(command);
|
|
25676
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25677
|
+
const result = await client.cancelSmsCampaign(id);
|
|
25678
|
+
emit(flags, result, () => {
|
|
25679
|
+
const { stats } = result.campaign;
|
|
25680
|
+
process.stdout.write(
|
|
25681
|
+
`Cancelled SMS campaign ${result.campaign.id} \u2014 ${stats.sent} recipient${stats.sent === 1 ? " was" : "s were"} already sent to before the cancel.
|
|
25682
|
+
`
|
|
25683
|
+
);
|
|
25684
|
+
});
|
|
25685
|
+
});
|
|
25686
|
+
smsCampaigns.command("recipients").description("List an SMS campaign's recipients (report drill-down), optionally by status").argument("<id>", "SMS campaign id").option("--status <status>", `filter to one status (${RECIPIENT_STATUSES2.join(" | ")})`).option("--limit <n>", "page size (1-200, default 50)").option("--cursor <cursor>", "cursor from a previous page").action(
|
|
25687
|
+
async (id, options, command) => {
|
|
25688
|
+
const flags = getGlobalFlags(command);
|
|
25689
|
+
let status;
|
|
25690
|
+
if (options.status !== void 0) {
|
|
25691
|
+
const match = RECIPIENT_STATUSES2.find((s) => s === options.status);
|
|
25692
|
+
if (!match) {
|
|
25693
|
+
throw new Error(
|
|
25694
|
+
`Invalid --status: ${options.status} (expected one of ${RECIPIENT_STATUSES2.join(", ")})`
|
|
25695
|
+
);
|
|
25696
|
+
}
|
|
25697
|
+
status = match;
|
|
25698
|
+
}
|
|
25699
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25700
|
+
const page = await client.listSmsCampaignRecipients(id, {
|
|
25701
|
+
status,
|
|
25702
|
+
limit: options.limit !== void 0 ? parsePositiveInt15(options.limit, "--limit") : void 0,
|
|
25703
|
+
cursor: options.cursor
|
|
25704
|
+
});
|
|
25705
|
+
emit(flags, page, () => {
|
|
25706
|
+
printTable(
|
|
25707
|
+
page.recipients.map((r) => ({
|
|
25708
|
+
id: r.id,
|
|
25709
|
+
phone: r.phoneNumber,
|
|
25710
|
+
name: [r.firstName, r.lastName].filter(Boolean).join(" "),
|
|
25711
|
+
status: r.status,
|
|
25712
|
+
parts: r.segments ?? "",
|
|
25713
|
+
processedAt: r.processedAt ?? "",
|
|
25714
|
+
details: r.statusDetails ?? ""
|
|
25715
|
+
})),
|
|
25716
|
+
["id", "phone", "name", "status", "parts", "processedAt", "details"]
|
|
25717
|
+
);
|
|
25718
|
+
if (page.nextCursor) {
|
|
25719
|
+
process.stdout.write(`
|
|
25720
|
+
Next page: --cursor ${page.nextCursor}
|
|
25721
|
+
`);
|
|
25722
|
+
}
|
|
25723
|
+
});
|
|
25724
|
+
}
|
|
25725
|
+
);
|
|
25726
|
+
}
|
|
25727
|
+
|
|
25728
|
+
// src/commands/social/index.ts
|
|
25729
|
+
var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
25730
|
+
function composeInput(options) {
|
|
25731
|
+
if (!options.text) {
|
|
25732
|
+
throw new Error("Provide the post text via --text.");
|
|
25733
|
+
}
|
|
25734
|
+
const targets = splitList(options.targets);
|
|
25735
|
+
if (targets.length === 0) {
|
|
25736
|
+
throw new Error("Provide at least one target via --targets (see `social accounts`).");
|
|
25737
|
+
}
|
|
25738
|
+
const mediaIds = splitList(options.media);
|
|
25739
|
+
return {
|
|
25740
|
+
text: options.text,
|
|
25741
|
+
targets,
|
|
25742
|
+
...mediaIds.length > 0 ? { mediaIds } : {},
|
|
25743
|
+
...options.firstComment ? { firstComment: options.firstComment } : {},
|
|
25744
|
+
...options.at ? { scheduledAt: options.at } : {},
|
|
25745
|
+
...options.publish ? { publish: true } : {}
|
|
25746
|
+
};
|
|
25747
|
+
}
|
|
25748
|
+
function registerSocialCommands(program2) {
|
|
24355
25749
|
const social = program2.command("social").description("Social media posts \u2014 connected accounts, drafts, scheduling, publishing");
|
|
24356
25750
|
social.command("accounts").description("List connected social accounts (the values to pass as --targets)").action(async (_, command) => {
|
|
24357
25751
|
const flags = getGlobalFlags(command);
|
|
@@ -24517,7 +25911,7 @@ function assertIsoDate3(value, flag) {
|
|
|
24517
25911
|
}
|
|
24518
25912
|
return value;
|
|
24519
25913
|
}
|
|
24520
|
-
function
|
|
25914
|
+
function parsePositiveInt16(value, flag) {
|
|
24521
25915
|
const n = Number.parseInt(value, 10);
|
|
24522
25916
|
if (!Number.isInteger(n) || n <= 0) {
|
|
24523
25917
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
@@ -24706,7 +26100,7 @@ function registerStatsCommands(program2) {
|
|
|
24706
26100
|
const flags = getGlobalFlags(command);
|
|
24707
26101
|
const range = {
|
|
24708
26102
|
...resolveRange(options),
|
|
24709
|
-
limit: options.limit !== void 0 ?
|
|
26103
|
+
limit: options.limit !== void 0 ? parsePositiveInt16(options.limit, "--limit") : void 0
|
|
24710
26104
|
};
|
|
24711
26105
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
24712
26106
|
const report = await client.statsSessions(range);
|
|
@@ -24767,7 +26161,7 @@ function registerStatsCommands(program2) {
|
|
|
24767
26161
|
sessionId: options.session,
|
|
24768
26162
|
statuses: options.status !== void 0 ? parseConversionStatuses(options.status) : void 0,
|
|
24769
26163
|
clients: options.client !== void 0 ? parseConversionClients(options.client) : void 0,
|
|
24770
|
-
limit: options.limit !== void 0 ?
|
|
26164
|
+
limit: options.limit !== void 0 ? parsePositiveInt16(options.limit, "--limit") : void 0
|
|
24771
26165
|
});
|
|
24772
26166
|
emit(flags, result, () => {
|
|
24773
26167
|
printTable(
|
|
@@ -24812,7 +26206,7 @@ function registerStatsCommands(program2) {
|
|
|
24812
26206
|
const flags = getGlobalFlags(command);
|
|
24813
26207
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
24814
26208
|
const result = await client.getConversionTrackingHealth(
|
|
24815
|
-
options.sinceDays !== void 0 ?
|
|
26209
|
+
options.sinceDays !== void 0 ? parsePositiveInt16(options.sinceDays, "--since-days") : void 0
|
|
24816
26210
|
);
|
|
24817
26211
|
emit(flags, result, () => {
|
|
24818
26212
|
process.stdout.write(`Conversion tracking health since ${result.since}
|
|
@@ -24878,7 +26272,7 @@ function describeAttempts(item) {
|
|
|
24878
26272
|
const next = item.nextPaymentAttempt !== null ? formatEpochSeconds(item.nextPaymentAttempt) : "";
|
|
24879
26273
|
return next ? `${attempts} (next ${next})` : String(attempts);
|
|
24880
26274
|
}
|
|
24881
|
-
function
|
|
26275
|
+
function requireConfirm13(confirm, what) {
|
|
24882
26276
|
if (!confirm) {
|
|
24883
26277
|
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
24884
26278
|
}
|
|
@@ -24952,7 +26346,7 @@ function registerSubscriptionsCommands(program2) {
|
|
|
24952
26346
|
"Retry a failed subscription charge by its Stripe invoice id (`in_\u2026` from `subscriptions failed`) \u2014 a REAL charge attempt on the customer's saved card. Requires --confirm"
|
|
24953
26347
|
).argument("<invoiceId>", "Stripe invoice id of the failed charge (in_\u2026)").option("--confirm", "required \u2014 acknowledge that the customer is charged for real").action(async (invoiceId, options, command) => {
|
|
24954
26348
|
const flags = getGlobalFlags(command);
|
|
24955
|
-
|
|
26349
|
+
requireConfirm13(options.confirm, `retry the charge for invoice ${invoiceId} (real charge)`);
|
|
24956
26350
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
24957
26351
|
const result = await client.retrySubscriptionPayment(invoiceId);
|
|
24958
26352
|
emit(flags, result, () => {
|
|
@@ -25171,7 +26565,7 @@ function parseChoice8(value, allowed, flag) {
|
|
|
25171
26565
|
}
|
|
25172
26566
|
return match;
|
|
25173
26567
|
}
|
|
25174
|
-
function
|
|
26568
|
+
function parsePositiveInt17(value, flag) {
|
|
25175
26569
|
const n = Number.parseInt(value, 10);
|
|
25176
26570
|
if (!Number.isInteger(n) || n <= 0) {
|
|
25177
26571
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
@@ -25303,7 +26697,7 @@ function registerTestimonialsCommands(program2) {
|
|
|
25303
26697
|
status: options.status !== void 0 ? parseChoice8(options.status, TESTIMONIAL_STATUSES, "--status") : void 0,
|
|
25304
26698
|
productId: options.product,
|
|
25305
26699
|
tag: options.tag,
|
|
25306
|
-
limit: options.limit !== void 0 ?
|
|
26700
|
+
limit: options.limit !== void 0 ? parsePositiveInt17(options.limit, "--limit") : void 0,
|
|
25307
26701
|
cursor: options.cursor
|
|
25308
26702
|
});
|
|
25309
26703
|
emit(flags, page, () => {
|
|
@@ -25489,22 +26883,180 @@ Next page: --cursor ${page.nextCursor}
|
|
|
25489
26883
|
}
|
|
25490
26884
|
|
|
25491
26885
|
// src/commands/workflows/index.ts
|
|
25492
|
-
import { readFileSync as
|
|
25493
|
-
|
|
26886
|
+
import { readFileSync as readFileSync25 } from "fs";
|
|
26887
|
+
|
|
26888
|
+
// src/commands/workflows/authoring.ts
|
|
26889
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
26890
|
+
var GRAPH_INVALID_STATUS = 422;
|
|
26891
|
+
function withGraphHint(error) {
|
|
26892
|
+
if (error instanceof ApiError && error.status === GRAPH_INVALID_STATUS) {
|
|
26893
|
+
throw new ApiError(
|
|
26894
|
+
error.status,
|
|
26895
|
+
error.apiMessage,
|
|
26896
|
+
"The workflow graph did not validate \u2014 re-run with -o json (or --verbose) to see every issue with its node id and reason code, fix the draft, then try again."
|
|
26897
|
+
);
|
|
26898
|
+
}
|
|
26899
|
+
throw error;
|
|
26900
|
+
}
|
|
26901
|
+
function describe(automation) {
|
|
26902
|
+
return `${automation.id} (${automation.name}, ${automation.status})`;
|
|
26903
|
+
}
|
|
26904
|
+
function registerWorkflowAuthoringCommands(workflows) {
|
|
26905
|
+
workflows.command("create").description(
|
|
26906
|
+
"Create a DRAFT workflow \u2014 nothing runs until `workflows publish`. Without --trigger the canvas starts empty; build the graph with `workflows draft set` or in the dashboard"
|
|
26907
|
+
).requiredOption("--name <name>", "workflow name").option("--description <text>", "what the workflow is for").option(
|
|
26908
|
+
"--product <ids>",
|
|
26909
|
+
"comma-separated product ids the workflow applies to (omit = workspace-wide)",
|
|
26910
|
+
parseCommaList("--product")
|
|
26911
|
+
).option(
|
|
26912
|
+
"--trigger <eventType>",
|
|
26913
|
+
"business event that starts it, e.g. product.ProductPurchased (see `workflows list` for examples)"
|
|
26914
|
+
).action(
|
|
26915
|
+
async (options, command) => {
|
|
26916
|
+
const flags = getGlobalFlags(command);
|
|
26917
|
+
const input = { name: options.name };
|
|
26918
|
+
if (options.description !== void 0) input.description = options.description;
|
|
26919
|
+
if (options.product !== void 0) input.productIds = options.product;
|
|
26920
|
+
if (options.trigger !== void 0) input.triggerEventType = options.trigger;
|
|
26921
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
26922
|
+
const result = await client.createAutomation(input);
|
|
26923
|
+
emit(flags, result, () => {
|
|
26924
|
+
process.stdout.write(
|
|
26925
|
+
`Created draft workflow ${describe(result.automation)}. Nothing runs until \`workflows publish\`.
|
|
26926
|
+
`
|
|
26927
|
+
);
|
|
26928
|
+
});
|
|
26929
|
+
}
|
|
26930
|
+
);
|
|
26931
|
+
const draft = workflows.command("draft").description("Edit a workflow's DRAFT graph (the published version keeps running untouched)");
|
|
26932
|
+
draft.command("get").description(
|
|
26933
|
+
"Print the draft graph as JSON \u2014 edit it and feed it back to `workflows draft set`. --published prints the live snapshot instead"
|
|
26934
|
+
).argument("<automationId>", "workflow id").option("--published", "print the PUBLISHED graph (what is running) instead of the draft").option("--out <file>", "write the graph JSON to a file instead of stdout").action(
|
|
26935
|
+
async (automationId, options, command) => {
|
|
26936
|
+
const flags = getGlobalFlags(command);
|
|
26937
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
26938
|
+
const workflow = await client.getAutomationGraph(automationId);
|
|
26939
|
+
if (workflow.draftGraph === void 0) {
|
|
26940
|
+
throw new Error(
|
|
26941
|
+
"This API does not return workflow graphs yet (it predates `?include=graph`) \u2014 update the API, or copy the graph from the dashboard builder."
|
|
26942
|
+
);
|
|
26943
|
+
}
|
|
26944
|
+
const graph = options.published ? workflow.publishedGraph : workflow.draftGraph;
|
|
26945
|
+
if (graph === null || graph === void 0) {
|
|
26946
|
+
throw new Error(
|
|
26947
|
+
`Workflow ${automationId} has never been published \u2014 there is no live graph.`
|
|
26948
|
+
);
|
|
26949
|
+
}
|
|
26950
|
+
const json = `${JSON.stringify(graph, null, 2)}
|
|
26951
|
+
`;
|
|
26952
|
+
if (options.out !== void 0) {
|
|
26953
|
+
writeFileSync6(options.out, json);
|
|
26954
|
+
emit(flags, { automationId, out: options.out }, () => {
|
|
26955
|
+
process.stdout.write(
|
|
26956
|
+
`Wrote the ${options.published ? "published" : "draft"} graph of ${automationId} to ${options.out}${workflow.hasUnpublishedChanges ? " (the draft has unpublished changes)" : ""}.
|
|
26957
|
+
`
|
|
26958
|
+
);
|
|
26959
|
+
});
|
|
26960
|
+
return;
|
|
26961
|
+
}
|
|
26962
|
+
process.stdout.write(json);
|
|
26963
|
+
}
|
|
26964
|
+
);
|
|
26965
|
+
draft.command("set").description(
|
|
26966
|
+
"Replace the whole draft graph ({ nodes, edges }) \u2014 the same JSON the dashboard builder saves. The PUBLISHED version is not affected until `workflows publish`"
|
|
26967
|
+
).argument("<automationId>", "workflow id").requiredOption(
|
|
26968
|
+
"--json <fileOrJson>",
|
|
26969
|
+
"the draft graph as a .json file path or an inline {\u2026} string",
|
|
26970
|
+
parseJsonObjectFileOrInline("--json")
|
|
26971
|
+
).option("--name <name>", "also rename the workflow").option("--description <text>", "also change the description").option(
|
|
26972
|
+
"--product <ids>",
|
|
26973
|
+
"also replace the product scope (comma-separated ids)",
|
|
26974
|
+
parseCommaList("--product")
|
|
26975
|
+
).action(
|
|
26976
|
+
async (automationId, options, command) => {
|
|
26977
|
+
const flags = getGlobalFlags(command);
|
|
26978
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
26979
|
+
const result = await client.updateAutomationDraft(automationId, {
|
|
26980
|
+
draft: options.json,
|
|
26981
|
+
...options.name !== void 0 ? { name: options.name } : {},
|
|
26982
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
26983
|
+
...options.product !== void 0 ? { productIds: options.product } : {}
|
|
26984
|
+
}).catch(withGraphHint);
|
|
26985
|
+
emit(flags, result, () => {
|
|
26986
|
+
if (result.issues.length > 0) {
|
|
26987
|
+
process.stdout.write(
|
|
26988
|
+
`Saved the draft of workflow ${automationId}, but it would NOT publish yet \u2014 ${result.issues.length} issue${result.issues.length === 1 ? "" : "s"}:
|
|
26989
|
+
`
|
|
26990
|
+
);
|
|
26991
|
+
for (const issue of result.issues) {
|
|
26992
|
+
const where = issue.nodeId ?? issue.edgeId;
|
|
26993
|
+
process.stdout.write(
|
|
26994
|
+
` - [${issue.reason}]${where ? ` ${where}:` : ""} ${issue.message}
|
|
26995
|
+
`
|
|
26996
|
+
);
|
|
26997
|
+
}
|
|
26998
|
+
return;
|
|
26999
|
+
}
|
|
27000
|
+
process.stdout.write(
|
|
27001
|
+
`Saved the draft of workflow ${automationId} \u2014 it validates. Publish it with \`workflows publish ${automationId} --confirm\`.
|
|
27002
|
+
`
|
|
27003
|
+
);
|
|
27004
|
+
});
|
|
27005
|
+
}
|
|
27006
|
+
);
|
|
27007
|
+
workflows.command("publish").description(
|
|
27008
|
+
"\u26A0\uFE0F Publishes the draft \u2014 the workflow goes LIVE: from now on REAL e-mails, SMS, webhooks and access changes fire on real customer events. The graph must validate. Requires --confirm"
|
|
27009
|
+
).argument("<automationId>", "workflow id").option("--confirm", "required \u2014 acknowledge the workflow goes live").action(async (automationId, options, command) => {
|
|
27010
|
+
const flags = getGlobalFlags(command);
|
|
27011
|
+
requireConfirm(options.confirm, `publish workflow ${automationId} (it goes live)`);
|
|
27012
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
27013
|
+
const result = await client.publishAutomation(automationId).catch(withGraphHint);
|
|
27014
|
+
emit(flags, result, () => {
|
|
27015
|
+
process.stdout.write(
|
|
27016
|
+
`Published workflow ${describe(result.automation)}. Pause it any time with \`workflows pause\`.
|
|
27017
|
+
`
|
|
27018
|
+
);
|
|
27019
|
+
});
|
|
27020
|
+
});
|
|
27021
|
+
workflows.command("duplicate").description("Duplicate a workflow \u2014 the copy is an unpublished draft").argument("<automationId>", "workflow id").option("--name <name>", "name of the copy (default: the original name with a copy suffix)").action(async (automationId, options, command) => {
|
|
27022
|
+
const flags = getGlobalFlags(command);
|
|
27023
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
27024
|
+
const result = await client.duplicateAutomation(automationId, options.name);
|
|
27025
|
+
emit(flags, result, () => {
|
|
27026
|
+
process.stdout.write(`Duplicated ${automationId} \u2192 ${describe(result.automation)}.
|
|
27027
|
+
`);
|
|
27028
|
+
});
|
|
27029
|
+
});
|
|
27030
|
+
workflows.command("delete").description(
|
|
27031
|
+
"PERMANENTLY delete a workflow \u2014 cannot be undone. `workflows pause` is the reversible alternative. Requires --confirm"
|
|
27032
|
+
).argument("<automationId>", "workflow id").option("--confirm", "required \u2014 acknowledge the permanent deletion").action(async (automationId, options, command) => {
|
|
27033
|
+
const flags = getGlobalFlags(command);
|
|
27034
|
+
requireConfirm(options.confirm, `delete workflow ${automationId}`);
|
|
27035
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
27036
|
+
const result = await client.deleteAutomation(automationId);
|
|
27037
|
+
emit(flags, result, () => {
|
|
27038
|
+
process.stdout.write(`Deleted workflow ${automationId}.
|
|
27039
|
+
`);
|
|
27040
|
+
});
|
|
27041
|
+
});
|
|
27042
|
+
}
|
|
27043
|
+
|
|
27044
|
+
// src/commands/workflows/index.ts
|
|
27045
|
+
function parsePositiveInt18(value, flag) {
|
|
25494
27046
|
const n = Number.parseInt(value, 10);
|
|
25495
27047
|
if (!Number.isInteger(n) || n <= 0) {
|
|
25496
27048
|
throw new Error(`Invalid ${flag}: ${value} (expected a positive integer)`);
|
|
25497
27049
|
}
|
|
25498
27050
|
return n;
|
|
25499
27051
|
}
|
|
25500
|
-
function
|
|
27052
|
+
function requireConfirm14(confirm, what) {
|
|
25501
27053
|
if (!confirm) {
|
|
25502
27054
|
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
25503
27055
|
}
|
|
25504
27056
|
}
|
|
25505
27057
|
function parseTriggerContext(value) {
|
|
25506
27058
|
const trimmed = value.trim();
|
|
25507
|
-
const raw = trimmed.startsWith("{") ? trimmed :
|
|
27059
|
+
const raw = trimmed.startsWith("{") ? trimmed : readFileSync25(value, "utf8");
|
|
25508
27060
|
let parsed;
|
|
25509
27061
|
try {
|
|
25510
27062
|
parsed = JSON.parse(raw);
|
|
@@ -25518,9 +27070,9 @@ function parseTriggerContext(value) {
|
|
|
25518
27070
|
}
|
|
25519
27071
|
return parsed;
|
|
25520
27072
|
}
|
|
25521
|
-
var
|
|
25522
|
-
function
|
|
25523
|
-
if (error instanceof ApiError && error.status ===
|
|
27073
|
+
var SCHEDULER_UNAVAILABLE_STATUS5 = 503;
|
|
27074
|
+
function withSchedulerHint5(error) {
|
|
27075
|
+
if (error instanceof ApiError && error.status === SCHEDULER_UNAVAILABLE_STATUS5) {
|
|
25524
27076
|
throw new ApiError(
|
|
25525
27077
|
error.status,
|
|
25526
27078
|
"Trigger.dev scheduler not configured \u2014 workflow runs cannot be started on this environment.",
|
|
@@ -25593,7 +27145,7 @@ function registerWorkflowsCommands(program2) {
|
|
|
25593
27145
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25594
27146
|
const page = await client.listAutomationRuns(
|
|
25595
27147
|
automationId,
|
|
25596
|
-
options.limit !== void 0 ?
|
|
27148
|
+
options.limit !== void 0 ? parsePositiveInt18(options.limit, "--limit") : void 0
|
|
25597
27149
|
);
|
|
25598
27150
|
emit(flags, page, () => {
|
|
25599
27151
|
printTable(
|
|
@@ -25635,9 +27187,9 @@ function registerWorkflowsCommands(program2) {
|
|
|
25635
27187
|
).argument("<automationId>", "workflow automation id").argument("<runId>", "run id (from `workflows runs`)").option("--confirm", "required \u2014 acknowledge that the workflow actions execute again").action(
|
|
25636
27188
|
async (automationId, runId, options, command) => {
|
|
25637
27189
|
const flags = getGlobalFlags(command);
|
|
25638
|
-
|
|
27190
|
+
requireConfirm14(options.confirm, `retry run ${runId} (its actions execute again)`);
|
|
25639
27191
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25640
|
-
const result = await client.retryAutomationRun(automationId, runId).catch(
|
|
27192
|
+
const result = await client.retryAutomationRun(automationId, runId).catch(withSchedulerHint5);
|
|
25641
27193
|
emit(flags, result, () => {
|
|
25642
27194
|
process.stdout.write(
|
|
25643
27195
|
`Accepted \u2014 retrying run ${runId} of workflow ${automationId} as new run ${result.runId}.
|
|
@@ -25659,7 +27211,7 @@ Follow it with: zanfia workflows runs ${automationId}
|
|
|
25659
27211
|
async (automationId, options, command) => {
|
|
25660
27212
|
const flags = getGlobalFlags(command);
|
|
25661
27213
|
const context = parseTriggerContext(options.context);
|
|
25662
|
-
|
|
27214
|
+
requireConfirm14(options.confirm, `test-run workflow ${automationId} (real actions fire)`);
|
|
25663
27215
|
process.stderr.write(
|
|
25664
27216
|
"Warning: a test run executes the published graph for real \u2014 emails are sent, tags applied, webhooks called. Point the context at a test client.\n"
|
|
25665
27217
|
);
|
|
@@ -25667,7 +27219,7 @@ Follow it with: zanfia workflows runs ${automationId}
|
|
|
25667
27219
|
const result = await client.testRunAutomation(automationId, {
|
|
25668
27220
|
context,
|
|
25669
27221
|
...options.triggerNode !== void 0 ? { triggerNodeId: options.triggerNode } : {}
|
|
25670
|
-
}).catch(
|
|
27222
|
+
}).catch(withSchedulerHint5);
|
|
25671
27223
|
emit(flags, result, () => {
|
|
25672
27224
|
process.stdout.write(
|
|
25673
27225
|
`Accepted \u2014 test run ${result.runId} started for workflow ${automationId}.
|
|
@@ -25677,6 +27229,7 @@ Follow it with: zanfia workflows runs ${automationId}
|
|
|
25677
27229
|
});
|
|
25678
27230
|
}
|
|
25679
27231
|
);
|
|
27232
|
+
registerWorkflowAuthoringCommands(workflows);
|
|
25680
27233
|
}
|
|
25681
27234
|
function formatProductScope(workflow) {
|
|
25682
27235
|
return workflow.productIds.length > 0 ? String(workflow.productIds.length) : "all";
|
|
@@ -25688,10 +27241,111 @@ function formatSteps(steps) {
|
|
|
25688
27241
|
return parts.join(", ");
|
|
25689
27242
|
}
|
|
25690
27243
|
|
|
27244
|
+
// src/commands/workspace/index.ts
|
|
27245
|
+
function parseOnOff3(value, flag) {
|
|
27246
|
+
const match = ["on", "off"].find((v) => v === value);
|
|
27247
|
+
if (!match) throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
|
|
27248
|
+
return match === "on";
|
|
27249
|
+
}
|
|
27250
|
+
function parseTranscriptionLanguage(value) {
|
|
27251
|
+
const match = TRANSCRIPTION_LANGUAGES.find((language) => language === value);
|
|
27252
|
+
if (!match) {
|
|
27253
|
+
throw new Error(
|
|
27254
|
+
`Invalid --transcription-language: ${value} (expected ${TRANSCRIPTION_LANGUAGES.join(" | ")})`
|
|
27255
|
+
);
|
|
27256
|
+
}
|
|
27257
|
+
return match;
|
|
27258
|
+
}
|
|
27259
|
+
function assertTimeZone(value) {
|
|
27260
|
+
try {
|
|
27261
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value });
|
|
27262
|
+
} catch {
|
|
27263
|
+
throw new Error(
|
|
27264
|
+
`Invalid --timezone: ${value} (expected an IANA name such as Europe/Warsaw or America/New_York)`
|
|
27265
|
+
);
|
|
27266
|
+
}
|
|
27267
|
+
}
|
|
27268
|
+
function printSettings(settings) {
|
|
27269
|
+
process.stdout.write(`Workspace ${settings.workspaceId}
|
|
27270
|
+
`);
|
|
27271
|
+
process.stdout.write(` Name: ${settings.workspaceName}
|
|
27272
|
+
`);
|
|
27273
|
+
process.stdout.write(
|
|
27274
|
+
` Timezone: ${settings.timezone ?? "(inherits the owner's account timezone)"}
|
|
27275
|
+
`
|
|
27276
|
+
);
|
|
27277
|
+
process.stdout.write(
|
|
27278
|
+
` Auto-transcribe uploads: ${settings.autoTranscribeUploads ? "on" : "off"}
|
|
27279
|
+
`
|
|
27280
|
+
);
|
|
27281
|
+
process.stdout.write(` Transcription language: ${settings.transcriptionLanguage}
|
|
27282
|
+
`);
|
|
27283
|
+
}
|
|
27284
|
+
function registerWorkspaceCommands(program2) {
|
|
27285
|
+
const workspace2 = program2.command("workspace").description(
|
|
27286
|
+
'Workspace-level settings: the name, the scheduling timezone (workflow "wait until" steps and other workspace-scoped scheduling) and the video transcription defaults'
|
|
27287
|
+
);
|
|
27288
|
+
workspace2.command("get", { isDefault: true }).description("Show the workspace settings").action(async (_options, command) => {
|
|
27289
|
+
const flags = getGlobalFlags(command);
|
|
27290
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
27291
|
+
const result = await client.getWorkspaceSettings();
|
|
27292
|
+
emit(flags, result, () => printSettings(result));
|
|
27293
|
+
});
|
|
27294
|
+
workspace2.command("update").description(
|
|
27295
|
+
"Change workspace settings. The video quality profile is plan-dependent and stays in the dashboard"
|
|
27296
|
+
).option("--name <name>", "workspace name (1-100 characters)").option("--timezone <iana>", "scheduling timezone, e.g. Europe/Warsaw").option("--inherit-timezone", "clear the override and follow the owner's account timezone").option("--auto-transcribe <on|off>", "transcribe newly uploaded videos automatically").option(
|
|
27297
|
+
"--transcription-language <language>",
|
|
27298
|
+
`default transcription language (${TRANSCRIPTION_LANGUAGES.join(" | ")})`
|
|
27299
|
+
).action(
|
|
27300
|
+
async (options, command) => {
|
|
27301
|
+
const flags = getGlobalFlags(command);
|
|
27302
|
+
if (options.timezone !== void 0 && options.inheritTimezone) {
|
|
27303
|
+
throw new Error("Use either --timezone or --inherit-timezone, not both.");
|
|
27304
|
+
}
|
|
27305
|
+
const input = {};
|
|
27306
|
+
if (options.name !== void 0) input.name = options.name;
|
|
27307
|
+
if (options.timezone !== void 0) {
|
|
27308
|
+
assertTimeZone(options.timezone);
|
|
27309
|
+
input.timezone = options.timezone;
|
|
27310
|
+
}
|
|
27311
|
+
if (options.inheritTimezone) input.timezone = null;
|
|
27312
|
+
if (options.autoTranscribe !== void 0) {
|
|
27313
|
+
input.autoTranscribeUploads = parseOnOff3(options.autoTranscribe, "--auto-transcribe");
|
|
27314
|
+
}
|
|
27315
|
+
if (options.transcriptionLanguage !== void 0) {
|
|
27316
|
+
input.transcriptionLanguage = parseTranscriptionLanguage(options.transcriptionLanguage);
|
|
27317
|
+
}
|
|
27318
|
+
if (Object.keys(input).length === 0) {
|
|
27319
|
+
throw new Error(
|
|
27320
|
+
"Nothing to update \u2014 pass at least one change flag (see `workspace update --help`)."
|
|
27321
|
+
);
|
|
27322
|
+
}
|
|
27323
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
27324
|
+
const result = await client.updateWorkspaceSettings(input);
|
|
27325
|
+
emit(flags, result, () => printSettings(result));
|
|
27326
|
+
}
|
|
27327
|
+
);
|
|
27328
|
+
}
|
|
27329
|
+
|
|
25691
27330
|
// src/program.ts
|
|
27331
|
+
function resolveCliVersion() {
|
|
27332
|
+
if ("0.3.47".length > 0) {
|
|
27333
|
+
return "0.3.47";
|
|
27334
|
+
}
|
|
27335
|
+
try {
|
|
27336
|
+
const raw = readFileSync26(new URL("../package.json", import.meta.url), "utf8");
|
|
27337
|
+
const parsed = JSON.parse(raw);
|
|
27338
|
+
if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
|
|
27339
|
+
const { version: version2 } = parsed;
|
|
27340
|
+
if (typeof version2 === "string") return version2;
|
|
27341
|
+
}
|
|
27342
|
+
} catch {
|
|
27343
|
+
}
|
|
27344
|
+
return "0.0.0";
|
|
27345
|
+
}
|
|
25692
27346
|
function createProgram(programName) {
|
|
25693
27347
|
const program2 = new Command();
|
|
25694
|
-
program2.name(programName).description("Zanfia CLI \u2014 manage products, clients, orders and more from your terminal").version(
|
|
27348
|
+
program2.name(programName).description("Zanfia CLI \u2014 manage products, clients, orders and more from your terminal").version(resolveCliVersion()).showHelpAfterError().addHelpText(
|
|
25695
27349
|
"after",
|
|
25696
27350
|
`
|
|
25697
27351
|
New to Zanfia?
|
|
@@ -25733,6 +27387,7 @@ Signing in \u2014 two roles, two consent pages:
|
|
|
25733
27387
|
registerConversationsCommands(program2);
|
|
25734
27388
|
registerClientsCommands(program2);
|
|
25735
27389
|
registerCampaignsCommands(program2);
|
|
27390
|
+
registerSmsCampaignsCommands(program2);
|
|
25736
27391
|
registerSegmentsCommands(program2);
|
|
25737
27392
|
registerFormsCommands(program2);
|
|
25738
27393
|
registerTestimonialsCommands(program2);
|
|
@@ -25745,6 +27400,7 @@ Signing in \u2014 two roles, two consent pages:
|
|
|
25745
27400
|
registerReferralsCommands(program2);
|
|
25746
27401
|
registerAdsCommands(program2);
|
|
25747
27402
|
registerWorkflowsCommands(program2);
|
|
27403
|
+
registerWorkspaceCommands(program2);
|
|
25748
27404
|
registerFunnelsCommands(program2);
|
|
25749
27405
|
registerPagesCommands(program2);
|
|
25750
27406
|
registerSocialCommands(program2);
|