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