@zanfia/cli 0.4.0 → 0.9.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 +52 -52
- package/dist/index.js +673 -179
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5249,9 +5249,9 @@ var require_lodash7 = __commonJS({
|
|
|
5249
5249
|
}
|
|
5250
5250
|
});
|
|
5251
5251
|
|
|
5252
|
-
// ../../node_modules/
|
|
5252
|
+
// ../../node_modules/lodash.uniqby/index.js
|
|
5253
5253
|
var require_lodash8 = __commonJS({
|
|
5254
|
-
"../../node_modules/
|
|
5254
|
+
"../../node_modules/lodash.uniqby/index.js"(exports, module) {
|
|
5255
5255
|
"use strict";
|
|
5256
5256
|
var baseIteratee = require_lodash4();
|
|
5257
5257
|
var baseUniq = require_lodash7();
|
|
@@ -6907,8 +6907,15 @@ import { readFileSync as readFileSync28 } from "fs";
|
|
|
6907
6907
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
6908
6908
|
import { homedir } from "os";
|
|
6909
6909
|
import { dirname, join } from "path";
|
|
6910
|
-
var
|
|
6911
|
-
var
|
|
6910
|
+
var CREDENTIALS_STORE_VERSION = 2;
|
|
6911
|
+
var MEMBER_PROFILE_PREFIX = "member:";
|
|
6912
|
+
var FALLBACK_PROFILE_KEY = "default";
|
|
6913
|
+
function configDir() {
|
|
6914
|
+
return process.env.ZANFIA_CONFIG_DIR || join(homedir(), ".config", "zanfia");
|
|
6915
|
+
}
|
|
6916
|
+
function credentialsFilePath() {
|
|
6917
|
+
return join(configDir(), "credentials.json");
|
|
6918
|
+
}
|
|
6912
6919
|
var orUndefined = (value) => value.length > 0 ? value : void 0;
|
|
6913
6920
|
var backendDefaults = {
|
|
6914
6921
|
apiUrl: true ? orUndefined("https://api.zanfia.com") : void 0,
|
|
@@ -6929,24 +6936,110 @@ function resolvePlatformUrl() {
|
|
|
6929
6936
|
const raw = process.env.ZANFIA_PLATFORM_URL || backendDefaults.platformUrl || "https://zanfia.co";
|
|
6930
6937
|
return raw.replace(/\/+$/, "");
|
|
6931
6938
|
}
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6939
|
+
var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6940
|
+
var isStoredCredentials = (value) => isRecord(value) && typeof value.apiKey === "string";
|
|
6941
|
+
var emptyStore = () => ({
|
|
6942
|
+
version: CREDENTIALS_STORE_VERSION,
|
|
6943
|
+
active: null,
|
|
6944
|
+
profiles: {}
|
|
6945
|
+
});
|
|
6946
|
+
function profileKeyFor(credentials) {
|
|
6947
|
+
const claims = decodeTokenClaims(credentials.apiKey);
|
|
6948
|
+
const kind = credentials.kind ?? claims.kind;
|
|
6949
|
+
const base = credentials.workspaceId ?? claims.workspaceId ?? FALLBACK_PROFILE_KEY;
|
|
6950
|
+
return kind === "member" ? `${MEMBER_PROFILE_PREFIX}${base}` : base;
|
|
6951
|
+
}
|
|
6952
|
+
function parseCredentialsStore(raw) {
|
|
6953
|
+
if (isStoredCredentials(raw)) {
|
|
6954
|
+
const key = profileKeyFor(raw);
|
|
6955
|
+
return { version: CREDENTIALS_STORE_VERSION, active: key, profiles: { [key]: raw } };
|
|
6956
|
+
}
|
|
6957
|
+
if (isRecord(raw) && isRecord(raw.profiles)) {
|
|
6958
|
+
const profiles = {};
|
|
6959
|
+
for (const [key, value] of Object.entries(raw.profiles)) {
|
|
6960
|
+
if (isStoredCredentials(value)) profiles[key] = value;
|
|
6938
6961
|
}
|
|
6939
|
-
|
|
6962
|
+
const keys = Object.keys(profiles);
|
|
6963
|
+
const active = typeof raw.active === "string" && raw.active in profiles ? raw.active : keys[0] ?? null;
|
|
6964
|
+
return { version: CREDENTIALS_STORE_VERSION, active, profiles };
|
|
6965
|
+
}
|
|
6966
|
+
return emptyStore();
|
|
6967
|
+
}
|
|
6968
|
+
function readCredentialsStore() {
|
|
6969
|
+
try {
|
|
6970
|
+
return parseCredentialsStore(JSON.parse(readFileSync(credentialsFilePath(), "utf8")));
|
|
6940
6971
|
} catch {
|
|
6941
|
-
return
|
|
6972
|
+
return emptyStore();
|
|
6942
6973
|
}
|
|
6943
6974
|
}
|
|
6975
|
+
function writeCredentialsStore(store) {
|
|
6976
|
+
const path2 = credentialsFilePath();
|
|
6977
|
+
mkdirSync(dirname(path2), { recursive: true, mode: 448 });
|
|
6978
|
+
writeFileSync(path2, JSON.stringify(store, null, 2), { mode: 384 });
|
|
6979
|
+
return path2;
|
|
6980
|
+
}
|
|
6981
|
+
function readCredentials() {
|
|
6982
|
+
const store = readCredentialsStore();
|
|
6983
|
+
return store.active ? store.profiles[store.active] ?? null : null;
|
|
6984
|
+
}
|
|
6944
6985
|
function writeCredentials(credentials) {
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6986
|
+
const store = readCredentialsStore();
|
|
6987
|
+
const key = profileKeyFor(credentials);
|
|
6988
|
+
store.profiles[key] = credentials;
|
|
6989
|
+
store.active = key;
|
|
6990
|
+
return writeCredentialsStore(store);
|
|
6991
|
+
}
|
|
6992
|
+
function listStoredProfiles() {
|
|
6993
|
+
const store = readCredentialsStore();
|
|
6994
|
+
return Object.entries(store.profiles).map(([key, credentials]) => ({ key, credentials, active: key === store.active })).sort((a, b) => {
|
|
6995
|
+
if (a.active !== b.active) return a.active ? -1 : 1;
|
|
6996
|
+
return (a.credentials.workspaceName ?? a.key).localeCompare(
|
|
6997
|
+
b.credentials.workspaceName ?? b.key
|
|
6998
|
+
);
|
|
6948
6999
|
});
|
|
6949
|
-
|
|
7000
|
+
}
|
|
7001
|
+
function findStoredProfile(store, ref) {
|
|
7002
|
+
const wanted = ref.trim().toLowerCase();
|
|
7003
|
+
if (!wanted) return null;
|
|
7004
|
+
const match = Object.entries(store.profiles).find(
|
|
7005
|
+
([key, credentials]) => [key, credentials.workspaceId, credentials.workspaceSlug, credentials.workspaceName].some(
|
|
7006
|
+
(candidate) => typeof candidate === "string" && candidate.toLowerCase() === wanted
|
|
7007
|
+
)
|
|
7008
|
+
);
|
|
7009
|
+
return match ? { key: match[0], credentials: match[1], active: match[0] === store.active } : null;
|
|
7010
|
+
}
|
|
7011
|
+
var StoredWorkspaceNotFoundError = class extends Error {
|
|
7012
|
+
constructor(ref, available) {
|
|
7013
|
+
super(
|
|
7014
|
+
`No stored login for workspace "${ref}".` + (available.length > 0 ? ` Stored: ${available.join(", ")}.` : "") + ` Log in to it with: zanfia login --workspace ${ref}`
|
|
7015
|
+
);
|
|
7016
|
+
this.name = "StoredWorkspaceNotFoundError";
|
|
7017
|
+
}
|
|
7018
|
+
};
|
|
7019
|
+
var profileLabel = (profile) => profile.credentials.workspaceSlug ?? profile.credentials.workspaceId ?? profile.key;
|
|
7020
|
+
function useStoredProfile(ref) {
|
|
7021
|
+
const store = readCredentialsStore();
|
|
7022
|
+
const profile = findStoredProfile(store, ref);
|
|
7023
|
+
if (!profile) {
|
|
7024
|
+
throw new StoredWorkspaceNotFoundError(
|
|
7025
|
+
ref,
|
|
7026
|
+
Object.entries(store.profiles).map(([key, credentials]) => profileLabel({ key, credentials }))
|
|
7027
|
+
);
|
|
7028
|
+
}
|
|
7029
|
+
store.active = profile.key;
|
|
7030
|
+
writeCredentialsStore(store);
|
|
7031
|
+
return { ...profile, active: true };
|
|
7032
|
+
}
|
|
7033
|
+
function apiKeyForStoredWorkspace(ref) {
|
|
7034
|
+
const store = readCredentialsStore();
|
|
7035
|
+
const profile = findStoredProfile(store, ref);
|
|
7036
|
+
if (!profile) {
|
|
7037
|
+
throw new StoredWorkspaceNotFoundError(
|
|
7038
|
+
ref,
|
|
7039
|
+
Object.entries(store.profiles).map(([key, credentials]) => profileLabel({ key, credentials }))
|
|
7040
|
+
);
|
|
7041
|
+
}
|
|
7042
|
+
return profile.credentials.apiKey;
|
|
6950
7043
|
}
|
|
6951
7044
|
function resolveApiKey(explicit) {
|
|
6952
7045
|
if (explicit) return explicit;
|
|
@@ -6995,15 +7088,19 @@ function isMockMode() {
|
|
|
6995
7088
|
}
|
|
6996
7089
|
return resolveApiUrl() === null;
|
|
6997
7090
|
}
|
|
6998
|
-
function
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
return
|
|
7091
|
+
function removeStoredLogin(options = {}) {
|
|
7092
|
+
const store = readCredentialsStore();
|
|
7093
|
+
const removed = store.active ? store.profiles[store.active] ?? null : null;
|
|
7094
|
+
if (options.all || Object.keys(store.profiles).length <= 1) {
|
|
7095
|
+
rmSync(credentialsFilePath(), { force: true });
|
|
7096
|
+
return { removed, nowActive: null };
|
|
7004
7097
|
}
|
|
7098
|
+
if (store.active) delete store.profiles[store.active];
|
|
7099
|
+
const [nextKey] = Object.entries(store.profiles).sort(([, a], [, b]) => b.savedAt.localeCompare(a.savedAt)).map(([key]) => key);
|
|
7100
|
+
store.active = nextKey ?? null;
|
|
7101
|
+
writeCredentialsStore(store);
|
|
7102
|
+
return { removed, nowActive: nextKey ? store.profiles[nextKey] ?? null : null };
|
|
7005
7103
|
}
|
|
7006
|
-
var credentialsPath = CREDENTIALS_FILE;
|
|
7007
7104
|
|
|
7008
7105
|
// src/utils/errors.ts
|
|
7009
7106
|
var ApiError = class extends Error {
|
|
@@ -7671,6 +7768,9 @@ function createMockClient() {
|
|
|
7671
7768
|
if (!result) throw new ApiError(404, `Product ${id} not found`);
|
|
7672
7769
|
return result;
|
|
7673
7770
|
},
|
|
7771
|
+
async deleteProduct() {
|
|
7772
|
+
throw new ApiError(501, "product delete is not available in mock mode");
|
|
7773
|
+
},
|
|
7674
7774
|
async setProductStatus() {
|
|
7675
7775
|
throw new ApiError(501, "product status changes are not available in mock mode");
|
|
7676
7776
|
},
|
|
@@ -8343,6 +8443,9 @@ function createMockClient() {
|
|
|
8343
8443
|
async cancelEmailCampaign() {
|
|
8344
8444
|
throw new ApiError(501, "email campaigns are not available in mock mode");
|
|
8345
8445
|
},
|
|
8446
|
+
async rescheduleEmailCampaign() {
|
|
8447
|
+
throw new ApiError(501, "email campaigns are not available in mock mode");
|
|
8448
|
+
},
|
|
8346
8449
|
async sendEmailCampaignTest() {
|
|
8347
8450
|
throw new ApiError(501, "email campaigns are not available in mock mode");
|
|
8348
8451
|
},
|
|
@@ -8514,6 +8617,9 @@ function createMockClient() {
|
|
|
8514
8617
|
async removeDownloadFile() {
|
|
8515
8618
|
throw new ApiError(501, "downloads content is not available in mock mode");
|
|
8516
8619
|
},
|
|
8620
|
+
async renameDownloadFile() {
|
|
8621
|
+
throw new ApiError(501, "downloads content is not available in mock mode");
|
|
8622
|
+
},
|
|
8517
8623
|
async reorderDownloadFiles() {
|
|
8518
8624
|
throw new ApiError(501, "downloads content is not available in mock mode");
|
|
8519
8625
|
},
|
|
@@ -9045,6 +9151,7 @@ function createHttpClient(config) {
|
|
|
9045
9151
|
createProduct: (input) => request("POST", "/products", input),
|
|
9046
9152
|
updateProduct: (id, input) => request("PATCH", `/products/${encodeURIComponent(id)}`, input),
|
|
9047
9153
|
archiveProduct: (id) => request("POST", `/products/${encodeURIComponent(id)}/archive`),
|
|
9154
|
+
deleteProduct: (id) => request("DELETE", `/products/${encodeURIComponent(id)}`),
|
|
9048
9155
|
setProductStatus: (id, status) => request("POST", `/products/${encodeURIComponent(id)}/status`, { status }),
|
|
9049
9156
|
listProductPrices: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/prices`),
|
|
9050
9157
|
createProductPrice: (productId, input) => request("POST", `/products/${encodeURIComponent(productId)}/prices`, input),
|
|
@@ -9123,6 +9230,8 @@ function createHttpClient(config) {
|
|
|
9123
9230
|
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
9124
9231
|
if (options?.cursor) params.set("cursor", options.cursor);
|
|
9125
9232
|
if (options?.products) params.set("products", options.products);
|
|
9233
|
+
if (options?.createdFrom) params.set("createdFrom", options.createdFrom);
|
|
9234
|
+
if (options?.createdTo) params.set("createdTo", options.createdTo);
|
|
9126
9235
|
const qs = params.toString();
|
|
9127
9236
|
return request("GET", `/clients${qs ? `?${qs}` : ""}`);
|
|
9128
9237
|
},
|
|
@@ -9538,6 +9647,7 @@ function createHttpClient(config) {
|
|
|
9538
9647
|
scheduledAt !== void 0 ? { scheduledAt } : {}
|
|
9539
9648
|
),
|
|
9540
9649
|
cancelEmailCampaign: (id) => request("POST", `/campaigns/${encodeURIComponent(id)}/cancel`),
|
|
9650
|
+
rescheduleEmailCampaign: (id, scheduledAt) => request("POST", `/campaigns/${encodeURIComponent(id)}/reschedule`, { scheduledAt }),
|
|
9541
9651
|
sendEmailCampaignTest: (id, recipients) => request(
|
|
9542
9652
|
"POST",
|
|
9543
9653
|
`/campaigns/${encodeURIComponent(id)}/test-sends`,
|
|
@@ -9669,6 +9779,11 @@ function createHttpClient(config) {
|
|
|
9669
9779
|
"DELETE",
|
|
9670
9780
|
`/products/${encodeURIComponent(productId)}/downloads/files/${encodeURIComponent(uid)}`
|
|
9671
9781
|
),
|
|
9782
|
+
renameDownloadFile: (productId, uid, name) => request(
|
|
9783
|
+
"PATCH",
|
|
9784
|
+
`/products/${encodeURIComponent(productId)}/downloads/files/${encodeURIComponent(uid)}`,
|
|
9785
|
+
{ name }
|
|
9786
|
+
),
|
|
9672
9787
|
reorderDownloadFiles: (productId, uids) => request("POST", `/products/${encodeURIComponent(productId)}/downloads/files/reorder`, {
|
|
9673
9788
|
uids
|
|
9674
9789
|
}),
|
|
@@ -10026,6 +10141,22 @@ function parseWorkflowKind(value) {
|
|
|
10026
10141
|
}
|
|
10027
10142
|
return kind;
|
|
10028
10143
|
}
|
|
10144
|
+
var STOREFRONT_VISIBILITIES = [
|
|
10145
|
+
"public",
|
|
10146
|
+
"unlisted",
|
|
10147
|
+
"private"
|
|
10148
|
+
];
|
|
10149
|
+
function parseStorefrontVisibility(value) {
|
|
10150
|
+
const visibility = STOREFRONT_VISIBILITIES.find(
|
|
10151
|
+
(candidate) => candidate === value.trim().toLowerCase()
|
|
10152
|
+
);
|
|
10153
|
+
if (visibility === void 0) {
|
|
10154
|
+
throw new Error(
|
|
10155
|
+
`Invalid --visibility: ${value} (expected one of: ${STOREFRONT_VISIBILITIES.join(", ")})`
|
|
10156
|
+
);
|
|
10157
|
+
}
|
|
10158
|
+
return visibility;
|
|
10159
|
+
}
|
|
10029
10160
|
function parseCommaList(flag) {
|
|
10030
10161
|
return (value) => {
|
|
10031
10162
|
const items = [
|
|
@@ -10065,6 +10196,8 @@ function requireConfirm(confirm, what) {
|
|
|
10065
10196
|
var OUTPUT_FORMAT_JSON = "json";
|
|
10066
10197
|
var API_KEY_FLAG = "--api-key <key>";
|
|
10067
10198
|
var API_KEY_DESCRIPTION = "override stored API key for this invocation";
|
|
10199
|
+
var WORKSPACE_FLAG = "--workspace <id|slug>";
|
|
10200
|
+
var WORKSPACE_DESCRIPTION = "use the stored login of this workspace for this invocation (see `workspace list`)";
|
|
10068
10201
|
var VERBOSE_FLAG = "--verbose";
|
|
10069
10202
|
var VERBOSE_DESCRIPTION = "log request/response debug info";
|
|
10070
10203
|
var JSON_FLAG = "--json";
|
|
@@ -10084,6 +10217,7 @@ function registerOnCommand(command) {
|
|
|
10084
10217
|
command.enablePositionalOptions();
|
|
10085
10218
|
}
|
|
10086
10219
|
addOptionIfMissing(command, new Option(API_KEY_FLAG, API_KEY_DESCRIPTION).hideHelp());
|
|
10220
|
+
addOptionIfMissing(command, new Option(WORKSPACE_FLAG, WORKSPACE_DESCRIPTION).hideHelp());
|
|
10087
10221
|
addOptionIfMissing(command, new Option(VERBOSE_FLAG, VERBOSE_DESCRIPTION).hideHelp());
|
|
10088
10222
|
addOptionIfMissing(
|
|
10089
10223
|
command,
|
|
@@ -10095,13 +10229,14 @@ function registerOnCommand(command) {
|
|
|
10095
10229
|
function applyGlobalOptions(program2) {
|
|
10096
10230
|
program2.enablePositionalOptions();
|
|
10097
10231
|
program2.option(API_KEY_FLAG, API_KEY_DESCRIPTION);
|
|
10232
|
+
program2.option(WORKSPACE_FLAG, WORKSPACE_DESCRIPTION);
|
|
10098
10233
|
program2.option(JSON_FLAG, JSON_DESCRIPTION);
|
|
10099
10234
|
program2.option(VERBOSE_FLAG, VERBOSE_DESCRIPTION);
|
|
10100
10235
|
for (const sub of program2.commands) registerOnCommand(sub);
|
|
10101
10236
|
}
|
|
10102
10237
|
|
|
10103
10238
|
// src/utils/output.ts
|
|
10104
|
-
function
|
|
10239
|
+
function isRecord2(value) {
|
|
10105
10240
|
return value !== null && typeof value === "object";
|
|
10106
10241
|
}
|
|
10107
10242
|
function getGlobalFlags(command) {
|
|
@@ -10112,11 +10247,15 @@ function getGlobalFlags(command) {
|
|
|
10112
10247
|
const flags = {};
|
|
10113
10248
|
for (const node of chain) {
|
|
10114
10249
|
const raw = node.opts();
|
|
10115
|
-
if (!
|
|
10250
|
+
if (!isRecord2(raw)) continue;
|
|
10116
10251
|
if (typeof raw.apiKey === "string") flags.apiKey = raw.apiKey;
|
|
10252
|
+
if (typeof raw.workspace === "string") flags.workspace = raw.workspace;
|
|
10117
10253
|
if (raw.verbose === true) flags.verbose = true;
|
|
10118
10254
|
if (raw.json === true || raw.output === OUTPUT_FORMAT_JSON) flags.json = true;
|
|
10119
10255
|
}
|
|
10256
|
+
if (flags.workspace && !flags.apiKey && !process.env.ZANFIA_API_KEY) {
|
|
10257
|
+
flags.apiKey = apiKeyForStoredWorkspace(flags.workspace);
|
|
10258
|
+
}
|
|
10120
10259
|
return flags;
|
|
10121
10260
|
}
|
|
10122
10261
|
function emit(flags, data, humanRenderer) {
|
|
@@ -10132,10 +10271,10 @@ function printJson(data) {
|
|
|
10132
10271
|
`);
|
|
10133
10272
|
}
|
|
10134
10273
|
function tableRowsOf(result, key) {
|
|
10135
|
-
if (!
|
|
10274
|
+
if (!isRecord2(result)) return [];
|
|
10136
10275
|
const rows = result[key];
|
|
10137
10276
|
if (!Array.isArray(rows)) return [];
|
|
10138
|
-
return rows.filter(
|
|
10277
|
+
return rows.filter(isRecord2);
|
|
10139
10278
|
}
|
|
10140
10279
|
function printTable(rows, columns) {
|
|
10141
10280
|
if (rows.length === 0) {
|
|
@@ -10185,11 +10324,11 @@ var listCertificateTemplatesColumns = ["id", "name", "version", "isDefault", "up
|
|
|
10185
10324
|
function registerCertificatesCommands(program2) {
|
|
10186
10325
|
const certificates = program2.command("certificates");
|
|
10187
10326
|
certificates.description(
|
|
10188
|
-
"Course-completion certificates \u2014 issue for course clients, revoke/reinstate, resend the email, and manage designer templates (`certificates templates \u2026`)"
|
|
10327
|
+
"Course-completion and event-participation certificates \u2014 issue for course or event clients, revoke/reinstate, resend the email, and manage designer templates (`certificates templates \u2026`)"
|
|
10189
10328
|
);
|
|
10190
10329
|
certificates.command("list").summary("List certificates").description(
|
|
10191
|
-
"Issued
|
|
10192
|
-
).option("--product <value>", "Course product id \u2014 omit for all
|
|
10330
|
+
"Issued certificates of the workspace (course completion and event participation), newest first, optionally filtered to one product (`productId`); `limit` caps the rows (service default applies). Each row is the credential: public `certificateId` (`cert_\u2026`, the id every certificate write takes), `productType` (`course` / `event`), recipient name/email, product (`courseName`) and issuer names, completion/issued dates, `issuedBy` (manual vs automatic), template id/version, PDF and verification URLs, `status` (active/revoked), `expiresAt` and delivery/engagement signals (email status, views, downloads, LinkedIn add). Also the polling endpoint after issue-certificates \u2014 a newly enqueued client shows up here once its PDF has been rendered."
|
|
10331
|
+
).option("--product <value>", "Course or event product id \u2014 omit for all products.").option(
|
|
10193
10332
|
"--limit <n>",
|
|
10194
10333
|
"Max rows, newest first (max 500; the service default applies when omitted).",
|
|
10195
10334
|
parseNumberOption("--limit", NumberFormat.Integer)
|
|
@@ -10206,17 +10345,17 @@ function registerCertificatesCommands(program2) {
|
|
|
10206
10345
|
emit(flags, result ?? {}, () => printTable(rows, listCertificatesColumns));
|
|
10207
10346
|
});
|
|
10208
10347
|
certificates.command("issue").summary("Issue certificates (async)").description(
|
|
10209
|
-
'Issues
|
|
10210
|
-
).argument("<productId>", "Course product id (
|
|
10348
|
+
'Issues certificates to the listed clients of ONE product: a course (completion certificate) or an event \u2014 webinar, workshop, also one held outside Zanfia (participation certificate, "for taking part in", no attendance check). The call validates the batch (404 unknown product or `templateId`, 400 when the product is neither a course nor an event), skips clients who already hold a certificate for it (one certificate per client per product, ever) and enqueues one issuance task per remaining client \u2014 PDF rendering and the "certificate issued" email happen asynchronously, so the response is 202 with `enqueued` / `skippedExistingCertificate` counts, not the certificates. Poll list-certificates (`productId` filter) a little later for the issued rows. A single issuance is this call with one `clientId`. With `onlyCompleted: true` a client only gets a certificate if their course progress is completed (checked per client at issuance time; courses only, 400 for an event). `clientIds` is capped at 1000 and de-duplicated. Every issuance EMAILS the student \u2014 the emails cannot be recalled.'
|
|
10349
|
+
).argument("<productId>", "Course or event product id (other product types answer 400).").requiredOption(
|
|
10211
10350
|
"--client <a,b,...>",
|
|
10212
10351
|
"Workspace client ids; one id = single issuance. Max 1000, de-duplicated.",
|
|
10213
10352
|
parseCommaList("--client")
|
|
10214
10353
|
).option(
|
|
10215
10354
|
"--only-completed",
|
|
10216
|
-
"
|
|
10355
|
+
"Course products only (400 for an event): skip clients who have not completed the course (checked per client at issuance time). Default false = issue to everyone listed."
|
|
10217
10356
|
).option("--no-only-completed", "Send `onlyCompleted: false` explicitly.").option(
|
|
10218
10357
|
"--template <value>",
|
|
10219
|
-
"Explicit template id; omitted = the
|
|
10358
|
+
"Explicit template id; omitted = the product template, else the workspace default."
|
|
10220
10359
|
).option(
|
|
10221
10360
|
"--reissue-revoked",
|
|
10222
10361
|
"Re-issue clients whose current certificate is REVOKED (new document in the current template, new number and verification link, the revoked one stays revoked). ACTIVE certificates are still skipped. Default false = any existing certificate skips the client."
|
|
@@ -10454,7 +10593,10 @@ function registerBlogCommands(program2) {
|
|
|
10454
10593
|
).option("--content <value>", "The body as Markdown (headings, lists, links, images by URL).").option(
|
|
10455
10594
|
"--excerpt <value>",
|
|
10456
10595
|
"Short plain-text summary (list, RSS, meta description); derived from the body on publish when empty."
|
|
10457
|
-
).option("--cover <value>", "Cover image URL (Media Library).").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option("--seo <json|file>", "seo", parseJsonObjectFileOrInline("--seo")).option("--publish", "Publish right away (needs a non-empty body); default false = draft.").option("--no-publish", "Send `publish: false` explicitly.").
|
|
10596
|
+
).option("--cover <value>", "Cover image URL (Media Library).").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option("--seo <json|file>", "seo", parseJsonObjectFileOrInline("--seo")).option("--publish", "Publish right away (needs a non-empty body); default false = draft.").option("--no-publish", "Send `publish: false` explicitly.").option(
|
|
10597
|
+
"--published-at <value>",
|
|
10598
|
+
"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."
|
|
10599
|
+
).action(async (options, command) => {
|
|
10458
10600
|
const flags = getGlobalFlags(command);
|
|
10459
10601
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
10460
10602
|
const params = {};
|
|
@@ -10466,7 +10608,8 @@ function registerBlogCommands(program2) {
|
|
|
10466
10608
|
coverImageUrl: options.cover,
|
|
10467
10609
|
tags: options.tags,
|
|
10468
10610
|
seo: options.seo,
|
|
10469
|
-
publish: options.publish
|
|
10611
|
+
publish: options.publish,
|
|
10612
|
+
publishedAt: options.publishedAt
|
|
10470
10613
|
};
|
|
10471
10614
|
const result = await client.call("POST", "/blog-posts", params, void 0, payload);
|
|
10472
10615
|
emit(flags, result ?? {}, () => printJson(result ?? {}));
|
|
@@ -10494,6 +10637,9 @@ function registerBlogCommands(program2) {
|
|
|
10494
10637
|
"--seo <json|file>",
|
|
10495
10638
|
"Replaces the whole SEO section.",
|
|
10496
10639
|
parseJsonObjectFileOrInline("--seo")
|
|
10640
|
+
).option(
|
|
10641
|
+
"--published-at <value>",
|
|
10642
|
+
"The post's date (ISO 8601, not in the future); null clears it on a draft - a published post keeps one."
|
|
10497
10643
|
).action(async (id, options, command) => {
|
|
10498
10644
|
const flags = getGlobalFlags(command);
|
|
10499
10645
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
@@ -10507,7 +10653,8 @@ function registerBlogCommands(program2) {
|
|
|
10507
10653
|
excerpt: options.excerpt,
|
|
10508
10654
|
coverImageUrl: options.cover,
|
|
10509
10655
|
tags: options.tags,
|
|
10510
|
-
seo: options.seo
|
|
10656
|
+
seo: options.seo,
|
|
10657
|
+
publishedAt: options.publishedAt
|
|
10511
10658
|
};
|
|
10512
10659
|
const result = await client.call("PATCH", "/blog-posts/:postId", params, void 0, payload);
|
|
10513
10660
|
emit(flags, result ?? {}, () => printJson(result ?? {}));
|
|
@@ -11158,16 +11305,93 @@ More ads available \u2014 re-run with --after ${result.after}
|
|
|
11158
11305
|
}
|
|
11159
11306
|
|
|
11160
11307
|
// src/commands/auth/index.ts
|
|
11308
|
+
function keySource(flags, rawApiKey) {
|
|
11309
|
+
if (typeof rawApiKey === "string") return "--api-key";
|
|
11310
|
+
if (process.env.ZANFIA_API_KEY) return "$ZANFIA_API_KEY";
|
|
11311
|
+
if (flags.workspace) return `stored login of --workspace ${flags.workspace}`;
|
|
11312
|
+
return "active stored login";
|
|
11313
|
+
}
|
|
11314
|
+
async function runWhoami(command) {
|
|
11315
|
+
const flags = getGlobalFlags(command);
|
|
11316
|
+
const rawApiKey = command.optsWithGlobals().apiKey;
|
|
11317
|
+
const logins = listStoredProfiles().length;
|
|
11318
|
+
const apiKey = resolveApiKey(flags.apiKey);
|
|
11319
|
+
if (decodeTokenKind(apiKey) === "member") {
|
|
11320
|
+
const stored = readCredentials();
|
|
11321
|
+
const claims = decodeTokenClaims(apiKey);
|
|
11322
|
+
const workspaceId = stored?.workspaceId ?? claims.workspaceId ?? "?";
|
|
11323
|
+
const info2 = {
|
|
11324
|
+
role: "member",
|
|
11325
|
+
workspaceId,
|
|
11326
|
+
workspaceName: stored?.workspaceName ?? null,
|
|
11327
|
+
userEmail: stored?.userEmail ?? null,
|
|
11328
|
+
uid: claims.subject ?? null,
|
|
11329
|
+
scopes: ["communities:read", "communities:write"],
|
|
11330
|
+
communityApiUrl: resolveCommunityApiUrl()
|
|
11331
|
+
};
|
|
11332
|
+
emit(flags, info2, () => {
|
|
11333
|
+
process.stdout.write(`Role: community member
|
|
11334
|
+
`);
|
|
11335
|
+
process.stdout.write(
|
|
11336
|
+
`Workspace: ${info2.workspaceName ?? "(unknown)"} (${info2.workspaceId})
|
|
11337
|
+
`
|
|
11338
|
+
);
|
|
11339
|
+
if (info2.userEmail) process.stdout.write(`User: ${info2.userEmail}
|
|
11340
|
+
`);
|
|
11341
|
+
else if (info2.uid) process.stdout.write(`User id: ${info2.uid}
|
|
11342
|
+
`);
|
|
11343
|
+
process.stdout.write(`Scopes: ${info2.scopes.join(", ")}
|
|
11344
|
+
`);
|
|
11345
|
+
process.stdout.write(`Community API: ${info2.communityApiUrl ?? "<unresolved>"}
|
|
11346
|
+
`);
|
|
11347
|
+
});
|
|
11348
|
+
return;
|
|
11349
|
+
}
|
|
11350
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
11351
|
+
const info = await client.whoami();
|
|
11352
|
+
const source = keySource(flags, rawApiKey);
|
|
11353
|
+
emit(flags, { ...info, keySource: source, storedLogins: logins }, () => {
|
|
11354
|
+
const modeTag = isMockMode() ? " (mock)" : "";
|
|
11355
|
+
process.stdout.write(`Workspace: ${info.workspaceName}${modeTag}
|
|
11356
|
+
`);
|
|
11357
|
+
process.stdout.write(`Workspace ID: ${info.workspaceId}
|
|
11358
|
+
`);
|
|
11359
|
+
process.stdout.write(`User: ${info.userEmail}
|
|
11360
|
+
`);
|
|
11361
|
+
process.stdout.write(`Scopes: ${info.scopes.join(", ")}
|
|
11362
|
+
`);
|
|
11363
|
+
process.stdout.write(`Key: ${source}
|
|
11364
|
+
`);
|
|
11365
|
+
if (logins > 1) {
|
|
11366
|
+
process.stdout.write(
|
|
11367
|
+
`Logins on this device: ${logins} (\`workspace list\`, switch with \`workspace use <id|slug>\`)
|
|
11368
|
+
`
|
|
11369
|
+
);
|
|
11370
|
+
}
|
|
11371
|
+
});
|
|
11372
|
+
}
|
|
11161
11373
|
function registerAuthCommands(program2) {
|
|
11162
11374
|
const auth = program2.command("auth").description("Manage CLI authentication");
|
|
11375
|
+
program2.command("whoami").description("Show the active workspace and identity (same as `auth whoami`)").action(async (_, command) => runWhoami(command));
|
|
11163
11376
|
auth.command("status").description("Show what the CLI currently has stored (path, source, expiry, api URL).").action(() => {
|
|
11164
11377
|
const stored = readCredentials();
|
|
11165
11378
|
const apiKey = resolveApiKey(void 0);
|
|
11166
11379
|
const apiUrl = resolveApiUrl();
|
|
11167
11380
|
const envKey = process.env.ZANFIA_API_KEY ? "(from $ZANFIA_API_KEY)" : "";
|
|
11168
11381
|
const envUrl = process.env.ZANFIA_API_URL ? "(from $ZANFIA_API_URL)" : "";
|
|
11169
|
-
|
|
11382
|
+
const profiles = listStoredProfiles();
|
|
11383
|
+
process.stdout.write(`Credentials file: ${credentialsFilePath()}
|
|
11384
|
+
`);
|
|
11385
|
+
if (profiles.length > 1) {
|
|
11386
|
+
process.stdout.write(`Stored logins: ${profiles.length} (active one below)
|
|
11170
11387
|
`);
|
|
11388
|
+
for (const profile of profiles) {
|
|
11389
|
+
process.stdout.write(
|
|
11390
|
+
` ${profile.active ? "*" : " "} ${profile.credentials.workspaceName ?? "?"} (${profile.key})
|
|
11391
|
+
`
|
|
11392
|
+
);
|
|
11393
|
+
}
|
|
11394
|
+
}
|
|
11171
11395
|
if (!stored) {
|
|
11172
11396
|
process.stdout.write(` status: no credentials file found
|
|
11173
11397
|
`);
|
|
@@ -11244,54 +11468,7 @@ Resolved by next call:
|
|
|
11244
11468
|
);
|
|
11245
11469
|
}
|
|
11246
11470
|
});
|
|
11247
|
-
auth.command("whoami").description("Show the authenticated identity (creator workspace, or community member)").action(async (_, command) =>
|
|
11248
|
-
const flags = getGlobalFlags(command);
|
|
11249
|
-
const apiKey = resolveApiKey(flags.apiKey);
|
|
11250
|
-
if (decodeTokenKind(apiKey) === "member") {
|
|
11251
|
-
const stored = readCredentials();
|
|
11252
|
-
const claims = decodeTokenClaims(apiKey);
|
|
11253
|
-
const workspaceId = stored?.workspaceId ?? claims.workspaceId ?? "?";
|
|
11254
|
-
const info2 = {
|
|
11255
|
-
role: "member",
|
|
11256
|
-
workspaceId,
|
|
11257
|
-
workspaceName: stored?.workspaceName ?? null,
|
|
11258
|
-
userEmail: stored?.userEmail ?? null,
|
|
11259
|
-
uid: claims.subject ?? null,
|
|
11260
|
-
scopes: ["communities:read", "communities:write"],
|
|
11261
|
-
communityApiUrl: resolveCommunityApiUrl()
|
|
11262
|
-
};
|
|
11263
|
-
emit(flags, info2, () => {
|
|
11264
|
-
process.stdout.write(`Role: community member
|
|
11265
|
-
`);
|
|
11266
|
-
process.stdout.write(
|
|
11267
|
-
`Workspace: ${info2.workspaceName ?? "(unknown)"} (${info2.workspaceId})
|
|
11268
|
-
`
|
|
11269
|
-
);
|
|
11270
|
-
if (info2.userEmail) process.stdout.write(`User: ${info2.userEmail}
|
|
11271
|
-
`);
|
|
11272
|
-
else if (info2.uid) process.stdout.write(`User id: ${info2.uid}
|
|
11273
|
-
`);
|
|
11274
|
-
process.stdout.write(`Scopes: ${info2.scopes.join(", ")}
|
|
11275
|
-
`);
|
|
11276
|
-
process.stdout.write(`Community API: ${info2.communityApiUrl ?? "<unresolved>"}
|
|
11277
|
-
`);
|
|
11278
|
-
});
|
|
11279
|
-
return;
|
|
11280
|
-
}
|
|
11281
|
-
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
11282
|
-
const info = await client.whoami();
|
|
11283
|
-
emit(flags, info, () => {
|
|
11284
|
-
const modeTag = isMockMode() ? " (mock)" : "";
|
|
11285
|
-
process.stdout.write(`Workspace: ${info.workspaceName}${modeTag}
|
|
11286
|
-
`);
|
|
11287
|
-
process.stdout.write(`Workspace ID: ${info.workspaceId}
|
|
11288
|
-
`);
|
|
11289
|
-
process.stdout.write(`User: ${info.userEmail}
|
|
11290
|
-
`);
|
|
11291
|
-
process.stdout.write(`Scopes: ${info.scopes.join(", ")}
|
|
11292
|
-
`);
|
|
11293
|
-
});
|
|
11294
|
-
});
|
|
11471
|
+
auth.command("whoami").description("Show the authenticated identity (creator workspace, or community member)").action(async (_, command) => runWhoami(command));
|
|
11295
11472
|
}
|
|
11296
11473
|
|
|
11297
11474
|
// src/commands/auth/login.ts
|
|
@@ -11490,25 +11667,54 @@ async function postJson(url, body) {
|
|
|
11490
11667
|
}
|
|
11491
11668
|
return JSON.parse(text);
|
|
11492
11669
|
}
|
|
11670
|
+
function buildCreatorConsentParams(input) {
|
|
11671
|
+
return {
|
|
11672
|
+
redirect_uri: input.redirectUri,
|
|
11673
|
+
state: input.state,
|
|
11674
|
+
code_challenge: input.challenge,
|
|
11675
|
+
code_challenge_method: "S256",
|
|
11676
|
+
hostname: input.hostname,
|
|
11677
|
+
client: "zanfia-cli",
|
|
11678
|
+
...input.workspace?.trim() ? { workspace: input.workspace.trim() } : null
|
|
11679
|
+
};
|
|
11680
|
+
}
|
|
11681
|
+
function buildRegisterUrl(consentUrl, email) {
|
|
11682
|
+
const consent = new URL(consentUrl);
|
|
11683
|
+
const continuePath = `${consent.pathname}${consent.search}`;
|
|
11684
|
+
const register = new URL("/auth/register", consent.origin);
|
|
11685
|
+
register.searchParams.set("continue", continuePath);
|
|
11686
|
+
register.searchParams.set("next", continuePath);
|
|
11687
|
+
if (email) register.searchParams.set("email", email);
|
|
11688
|
+
return register.toString();
|
|
11689
|
+
}
|
|
11493
11690
|
async function runLogin(options) {
|
|
11691
|
+
await runBrowserAuth(options, (consentUrl) => consentUrl);
|
|
11692
|
+
}
|
|
11693
|
+
async function runBrowserRegister(options) {
|
|
11694
|
+
await runBrowserAuth(options, (consentUrl) => buildRegisterUrl(consentUrl, options.email));
|
|
11695
|
+
}
|
|
11696
|
+
async function runBrowserAuth(options, entryUrl) {
|
|
11494
11697
|
const urls = resolveUrls(options.apiUrl);
|
|
11495
11698
|
const state = randomBytes(32).toString("base64url");
|
|
11496
11699
|
const { verifier, challenge } = generatePkce();
|
|
11497
11700
|
const host = safeHostname();
|
|
11498
11701
|
const server = await startLoopbackServer();
|
|
11499
11702
|
const redirectUri = `http://127.0.0.1:${server.port}${CALLBACK_PATH}`;
|
|
11500
|
-
const consentUrl = buildConsentUrl(
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11703
|
+
const consentUrl = buildConsentUrl(
|
|
11704
|
+
urls.loginUrl,
|
|
11705
|
+
buildCreatorConsentParams({
|
|
11706
|
+
redirectUri,
|
|
11707
|
+
state,
|
|
11708
|
+
challenge,
|
|
11709
|
+
hostname: host,
|
|
11710
|
+
workspace: options.workspace
|
|
11711
|
+
})
|
|
11712
|
+
);
|
|
11713
|
+
const openUrl = entryUrl(consentUrl);
|
|
11714
|
+
process.stdout.write(`Opening ${openUrl}
|
|
11509
11715
|
`);
|
|
11510
11716
|
process.stdout.write("Waiting for browser callback...\n\n");
|
|
11511
|
-
const opened = await openBrowser(
|
|
11717
|
+
const opened = await openBrowser(openUrl);
|
|
11512
11718
|
if (!opened) {
|
|
11513
11719
|
process.stdout.write("Could not open browser automatically \u2014 paste the URL above.\n\n");
|
|
11514
11720
|
}
|
|
@@ -11532,14 +11738,24 @@ async function runLogin(options) {
|
|
|
11532
11738
|
expiresAt: exchanged.expiresAt,
|
|
11533
11739
|
userEmail: exchanged.userEmail,
|
|
11534
11740
|
workspaceId: exchanged.workspaceId,
|
|
11535
|
-
workspaceName: exchanged.workspaceName
|
|
11741
|
+
workspaceName: exchanged.workspaceName,
|
|
11742
|
+
workspaceSlug: exchanged.workspaceSlug ?? null
|
|
11536
11743
|
});
|
|
11537
11744
|
process.stdout.write(`Logged in as ${exchanged.userEmail}.
|
|
11538
11745
|
`);
|
|
11539
|
-
process.stdout.write(
|
|
11540
|
-
`)
|
|
11746
|
+
process.stdout.write(
|
|
11747
|
+
`Workspace: ${exchanged.workspaceName} (${exchanged.workspaceId}) - now active
|
|
11748
|
+
`
|
|
11749
|
+
);
|
|
11541
11750
|
process.stdout.write(`Credentials stored at ${stored}
|
|
11542
11751
|
`);
|
|
11752
|
+
const logins = listStoredProfiles().length;
|
|
11753
|
+
if (logins > 1) {
|
|
11754
|
+
process.stdout.write(
|
|
11755
|
+
`Logins on this device: ${logins}. Switch with \`zanfia workspace use <id|slug>\` (see \`zanfia workspace list\`).
|
|
11756
|
+
`
|
|
11757
|
+
);
|
|
11758
|
+
}
|
|
11543
11759
|
if (!urls.apiUrl) {
|
|
11544
11760
|
process.stdout.write(
|
|
11545
11761
|
"\nTip: ZANFIA_API_URL is not set. Pass --api-url <url> or export the env var so commands like `zanfia products list` know where to call.\n"
|
|
@@ -11652,6 +11868,9 @@ function registerLoginCommand(auth) {
|
|
|
11652
11868
|
auth.command("login").description(
|
|
11653
11869
|
"Open a browser to authorize the CLI via OAuth (pass --address to sign in as a community member)"
|
|
11654
11870
|
).option("--api-url <url>", "API URL to associate with the resulting token").option(
|
|
11871
|
+
"--workspace <id|slug>",
|
|
11872
|
+
"preselect this workspace on the consent page (you can still pick another there)"
|
|
11873
|
+
).option(
|
|
11655
11874
|
"--address <url>",
|
|
11656
11875
|
"sign in as a community member (Community API) against this creator platform URL"
|
|
11657
11876
|
).option("-v, --verbose", "show HTTP request details").addHelpText(
|
|
@@ -11660,9 +11879,13 @@ function registerLoginCommand(auth) {
|
|
|
11660
11879
|
The two flows use different consent pages:
|
|
11661
11880
|
|
|
11662
11881
|
Creator (workspace) login \u2014 full Workspace API:
|
|
11663
|
-
login
|
|
11882
|
+
login [--workspace <id|slug>]
|
|
11664
11883
|
Opens the consent page in the Zanfia dashboard. Sign in with your
|
|
11665
|
-
creator account and approve; the CLI
|
|
11884
|
+
creator account, pick the workspace (Owner or Admin) and approve; the CLI
|
|
11885
|
+
gets a token for that workspace. Each workspace keeps its own login on
|
|
11886
|
+
this device: \`workspace list\` shows them, \`workspace use <id|slug>\`
|
|
11887
|
+
switches without logging in again, and \`--workspace <id|slug>\` on any
|
|
11888
|
+
command uses another stored login just for that command.
|
|
11666
11889
|
|
|
11667
11890
|
Community member login \u2014 Community API only:
|
|
11668
11891
|
login --address https://<creator-platform-url>
|
|
@@ -11686,14 +11909,27 @@ The two flows use different consent pages:
|
|
|
11686
11909
|
await runLogin(options);
|
|
11687
11910
|
});
|
|
11688
11911
|
}
|
|
11689
|
-
function registerLogoutCommand(auth
|
|
11690
|
-
auth.command("logout").description(
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
} else {
|
|
11912
|
+
function registerLogoutCommand(auth) {
|
|
11913
|
+
auth.command("logout").description(
|
|
11914
|
+
"Forget the active stored login (--all forgets every workspace login on this device)"
|
|
11915
|
+
).option("--all", "remove every stored login").action((options) => {
|
|
11916
|
+
const { removed, nowActive } = removeStoredLogin({ all: options.all });
|
|
11917
|
+
if (!removed && !options.all) {
|
|
11696
11918
|
process.stdout.write("No stored credentials to remove.\n");
|
|
11919
|
+
return;
|
|
11920
|
+
}
|
|
11921
|
+
if (options.all) {
|
|
11922
|
+
process.stdout.write("Removed every stored login.\n");
|
|
11923
|
+
return;
|
|
11924
|
+
}
|
|
11925
|
+
const label = removed?.workspaceName ?? removed?.workspaceId ?? "the stored login";
|
|
11926
|
+
process.stdout.write(`Logged out of ${label}.
|
|
11927
|
+
`);
|
|
11928
|
+
if (nowActive) {
|
|
11929
|
+
process.stdout.write(
|
|
11930
|
+
`Active now: ${nowActive.workspaceName ?? "?"} (${nowActive.workspaceId ?? "?"})
|
|
11931
|
+
`
|
|
11932
|
+
);
|
|
11697
11933
|
}
|
|
11698
11934
|
});
|
|
11699
11935
|
}
|
|
@@ -11703,6 +11939,16 @@ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
|
11703
11939
|
|
|
11704
11940
|
// src/utils/httpJson.ts
|
|
11705
11941
|
import { hostname as hostname2 } from "os";
|
|
11942
|
+
var HttpJsonError = class extends Error {
|
|
11943
|
+
status;
|
|
11944
|
+
code;
|
|
11945
|
+
constructor(message, status, code) {
|
|
11946
|
+
super(message);
|
|
11947
|
+
this.name = "HttpJsonError";
|
|
11948
|
+
this.status = status;
|
|
11949
|
+
this.code = code;
|
|
11950
|
+
}
|
|
11951
|
+
};
|
|
11706
11952
|
async function postJson2(label, url, body, options = {}) {
|
|
11707
11953
|
if (options.verbose) process.stderr.write(`\u2192 POST ${url}
|
|
11708
11954
|
`);
|
|
@@ -11718,8 +11964,10 @@ async function postJson2(label, url, body, options = {}) {
|
|
|
11718
11964
|
});
|
|
11719
11965
|
const text = await response.text();
|
|
11720
11966
|
if (!response.ok) {
|
|
11721
|
-
throw new
|
|
11722
|
-
`${label} failed (${response.status}): ${extractErrorMessage(text, response.statusText)}
|
|
11967
|
+
throw new HttpJsonError(
|
|
11968
|
+
`${label} failed (${response.status}): ${extractErrorMessage(text, response.statusText)}`,
|
|
11969
|
+
response.status,
|
|
11970
|
+
extractErrorCode(text)
|
|
11723
11971
|
);
|
|
11724
11972
|
}
|
|
11725
11973
|
return JSON.parse(text);
|
|
@@ -11734,6 +11982,14 @@ function extractErrorMessage(text, fallback) {
|
|
|
11734
11982
|
}
|
|
11735
11983
|
return text || fallback;
|
|
11736
11984
|
}
|
|
11985
|
+
function extractErrorCode(text) {
|
|
11986
|
+
try {
|
|
11987
|
+
const parsed = JSON.parse(text);
|
|
11988
|
+
return typeof parsed.code === "string" ? parsed.code : null;
|
|
11989
|
+
} catch {
|
|
11990
|
+
return null;
|
|
11991
|
+
}
|
|
11992
|
+
}
|
|
11737
11993
|
function safeHostname2() {
|
|
11738
11994
|
try {
|
|
11739
11995
|
const h = hostname2();
|
|
@@ -11790,6 +12046,7 @@ function promptHidden(question) {
|
|
|
11790
12046
|
}
|
|
11791
12047
|
|
|
11792
12048
|
// src/commands/auth/register.ts
|
|
12049
|
+
var BOT_CHECK_FAILED_CODE = "auth/bot-check-failed";
|
|
11793
12050
|
var MAX_CODE_ATTEMPTS = 5;
|
|
11794
12051
|
var MIN_PASSWORD_LENGTH = 8;
|
|
11795
12052
|
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -11833,6 +12090,32 @@ async function runRegister(options) {
|
|
|
11833
12090
|
if (!EMAIL_REGEX.test(email)) {
|
|
11834
12091
|
throw new Error(`"${email}" does not look like a valid email address.`);
|
|
11835
12092
|
}
|
|
12093
|
+
const initiateBody = {
|
|
12094
|
+
email,
|
|
12095
|
+
lang,
|
|
12096
|
+
accessCode: options.accessCode ?? null
|
|
12097
|
+
};
|
|
12098
|
+
try {
|
|
12099
|
+
await postJson2(
|
|
12100
|
+
"Sending verification code",
|
|
12101
|
+
`${urls.usersUrl}/initiate-registration`,
|
|
12102
|
+
initiateBody,
|
|
12103
|
+
{ verbose }
|
|
12104
|
+
);
|
|
12105
|
+
} catch (error) {
|
|
12106
|
+
if (error instanceof HttpJsonError && error.code === BOT_CHECK_FAILED_CODE) {
|
|
12107
|
+
process.stdout.write(
|
|
12108
|
+
"\nNew Zanfia accounts finish signing up in the browser (a quick security check).\nOpening the signup page; the CLI logs in once you are done.\n\n"
|
|
12109
|
+
);
|
|
12110
|
+
await runBrowserRegister({ email, apiUrl: options.apiUrl, verbose });
|
|
12111
|
+
return;
|
|
12112
|
+
}
|
|
12113
|
+
throw error;
|
|
12114
|
+
}
|
|
12115
|
+
process.stdout.write(`
|
|
12116
|
+
We sent a 6-digit code to ${email}. Check your inbox.
|
|
12117
|
+
|
|
12118
|
+
`);
|
|
11836
12119
|
const fullName = options.name ?? await promptLine("Full name: ");
|
|
11837
12120
|
if (fullName.length < 3 || fullName.length > 100) {
|
|
11838
12121
|
throw new Error("Full name must be 3-100 characters.");
|
|
@@ -11859,21 +12142,6 @@ async function runRegister(options) {
|
|
|
11859
12142
|
} else if (password.length < MIN_PASSWORD_LENGTH) {
|
|
11860
12143
|
throw new Error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters.`);
|
|
11861
12144
|
}
|
|
11862
|
-
const initiateBody = {
|
|
11863
|
-
email,
|
|
11864
|
-
lang,
|
|
11865
|
-
accessCode: options.accessCode ?? null
|
|
11866
|
-
};
|
|
11867
|
-
await postJson2(
|
|
11868
|
-
"Sending verification code",
|
|
11869
|
-
`${urls.usersUrl}/initiate-registration`,
|
|
11870
|
-
initiateBody,
|
|
11871
|
-
{ verbose }
|
|
11872
|
-
);
|
|
11873
|
-
process.stdout.write(`
|
|
11874
|
-
We sent a 6-digit code to ${email}. Check your inbox.
|
|
11875
|
-
|
|
11876
|
-
`);
|
|
11877
12145
|
let verificationToken = null;
|
|
11878
12146
|
for (let attempt = 1; attempt <= MAX_CODE_ATTEMPTS; attempt++) {
|
|
11879
12147
|
const code = options.code ?? await promptLine("Verification code (or 'r' to resend): ");
|
|
@@ -11978,16 +12246,18 @@ Logged in as ${exchanged.userEmail}.
|
|
|
11978
12246
|
}
|
|
11979
12247
|
}
|
|
11980
12248
|
function registerRegisterCommand(program2) {
|
|
11981
|
-
program2.command("register").description("Create a new Zanfia creator account
|
|
12249
|
+
program2.command("register").description("Create a new Zanfia creator account and log the CLI in").option("--email <email>", "account email (prompted when omitted)").option("--name <fullName>", "your full name (prompted when omitted)").option(
|
|
11982
12250
|
"--password <password>",
|
|
11983
12251
|
"optional account password (min 8 chars); prefer the interactive prompt \u2014 flags leak into shell history"
|
|
11984
12252
|
).option("--lang <lang>", "account language (en|pl)", "en").option("--access-code <code>", "signup access code (only when signups are gated)").option("--code <code>", "email verification code (skips the interactive prompt)").option("--api-url <url>", "API URL to associate with the resulting token").option("-v, --verbose", "show HTTP request details").addHelpText(
|
|
11985
12253
|
"after",
|
|
11986
12254
|
`
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
run \`login
|
|
12255
|
+
New accounts finish signing up in the browser: Zanfia runs a quick security
|
|
12256
|
+
check on the signup page, which a terminal cannot do. The CLI opens the
|
|
12257
|
+
signup page with your email filled in, and once the account exists it logs
|
|
12258
|
+
in exactly as if you had run \`login\`. Where the check does not apply, the
|
|
12259
|
+
whole flow stays in the terminal: a 6-digit code by email, then the account
|
|
12260
|
+
plus its personal workspace are created.
|
|
11991
12261
|
|
|
11992
12262
|
Running it with an email that already has a creator account simply signs
|
|
11993
12263
|
that account in.
|
|
@@ -12608,6 +12878,28 @@ function registerCampaignsCommands(program2) {
|
|
|
12608
12878
|
const { stats } = result.campaign;
|
|
12609
12879
|
process.stdout.write(
|
|
12610
12880
|
`Cancelled campaign ${result.campaign.id} \u2014 ${stats.sent} recipient${stats.sent === 1 ? " was" : "s were"} already sent to before the cancel.
|
|
12881
|
+
`
|
|
12882
|
+
);
|
|
12883
|
+
});
|
|
12884
|
+
});
|
|
12885
|
+
campaigns.command("reschedule").description(
|
|
12886
|
+
"Move a SCHEDULED campaign to a new send time (min 2 minutes, max 30 days out) \u2014 it stays scheduled and goes out only at the new time. Drafts: `campaigns send --at` instead."
|
|
12887
|
+
).argument("<id>", "campaign id (must be scheduled)").requiredOption(
|
|
12888
|
+
"--at <isoTimestamp>",
|
|
12889
|
+
"new send time (ISO with offset, e.g. 2026-10-15T09:00:00+02:00)"
|
|
12890
|
+
).action(async (id, options, command) => {
|
|
12891
|
+
const flags = getGlobalFlags(command);
|
|
12892
|
+
if (Number.isNaN(Date.parse(options.at))) {
|
|
12893
|
+
throw new Error(
|
|
12894
|
+
`Invalid --at timestamp: ${options.at} (expected ISO, e.g. 2026-10-15T09:00:00+02:00)`
|
|
12895
|
+
);
|
|
12896
|
+
}
|
|
12897
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
12898
|
+
const result = await client.rescheduleEmailCampaign(id, options.at);
|
|
12899
|
+
emit(flags, result, () => {
|
|
12900
|
+
const { campaign } = result;
|
|
12901
|
+
process.stdout.write(
|
|
12902
|
+
`Campaign ${campaign.id} moved to ${campaign.scheduledAt} \u2014 still scheduled; stop it before then with \`campaigns cancel\`.
|
|
12611
12903
|
`
|
|
12612
12904
|
);
|
|
12613
12905
|
});
|
|
@@ -12964,6 +13256,11 @@ function buildConsentInput(options) {
|
|
|
12964
13256
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
12965
13257
|
|
|
12966
13258
|
// src/client/types.ts
|
|
13259
|
+
var GRANT_PAYMENT_METHODS = [
|
|
13260
|
+
"bankTransfer",
|
|
13261
|
+
"cash",
|
|
13262
|
+
"other"
|
|
13263
|
+
];
|
|
12967
13264
|
var AIImageAspectRatio = /* @__PURE__ */ ((AIImageAspectRatio2) => {
|
|
12968
13265
|
AIImageAspectRatio2["Square"] = "1:1";
|
|
12969
13266
|
AIImageAspectRatio2["Portrait"] = "4:5";
|
|
@@ -13492,6 +13789,40 @@ function registerClientTagsCommands(clients2) {
|
|
|
13492
13789
|
var IMPORT_CHUNK_SIZE = 100;
|
|
13493
13790
|
var SCHEDULER_UNAVAILABLE_STATUS = 503;
|
|
13494
13791
|
var MAX_BULK_FILTERED_PRODUCTS = 10;
|
|
13792
|
+
function parseGrantPayment(options) {
|
|
13793
|
+
const { paid, currency, paidAt, paymentMethod, paymentNote } = options;
|
|
13794
|
+
if (paid === void 0) {
|
|
13795
|
+
if (currency || paidAt || paymentMethod || paymentNote) {
|
|
13796
|
+
throw new Error("--currency, --paid-at, --payment-method and --payment-note need --paid.");
|
|
13797
|
+
}
|
|
13798
|
+
return void 0;
|
|
13799
|
+
}
|
|
13800
|
+
if (!/^\d+([.,]\d{1,2})?$/.test(paid)) {
|
|
13801
|
+
throw new Error(`Invalid --paid amount: ${paid} (expected e.g. 99 or 99.50)`);
|
|
13802
|
+
}
|
|
13803
|
+
const amountCents = Math.round(Number(paid.replace(",", ".")) * 100);
|
|
13804
|
+
if (amountCents <= 0) {
|
|
13805
|
+
throw new Error("--paid must be above 0.");
|
|
13806
|
+
}
|
|
13807
|
+
if (!currency) {
|
|
13808
|
+
throw new Error("--paid needs --currency (pln, eur, usd or gbp).");
|
|
13809
|
+
}
|
|
13810
|
+
if (paidAt !== void 0 && Number.isNaN(Date.parse(paidAt))) {
|
|
13811
|
+
throw new Error(`Invalid --paid-at date: ${paidAt} (expected ISO, e.g. 2026-10-14)`);
|
|
13812
|
+
}
|
|
13813
|
+
if (paymentMethod !== void 0 && !GRANT_PAYMENT_METHODS.includes(paymentMethod)) {
|
|
13814
|
+
throw new Error(
|
|
13815
|
+
`Invalid --payment-method: ${paymentMethod} (one of ${GRANT_PAYMENT_METHODS.join(", ")})`
|
|
13816
|
+
);
|
|
13817
|
+
}
|
|
13818
|
+
return {
|
|
13819
|
+
amountCents,
|
|
13820
|
+
currency: currency.toLowerCase(),
|
|
13821
|
+
...paidAt !== void 0 ? { paidAt: new Date(paidAt).toISOString() } : {},
|
|
13822
|
+
...paymentMethod !== void 0 ? { method: paymentMethod } : {},
|
|
13823
|
+
...paymentNote !== void 0 ? { note: paymentNote } : {}
|
|
13824
|
+
};
|
|
13825
|
+
}
|
|
13495
13826
|
function requireConfirm4(confirm, what) {
|
|
13496
13827
|
if (!confirm) {
|
|
13497
13828
|
throw new Error(`Refusing to ${what} without --confirm. Re-run with --confirm to proceed.`);
|
|
@@ -13756,7 +14087,15 @@ function printMergePreview(preview) {
|
|
|
13756
14087
|
}
|
|
13757
14088
|
function registerClientsCommands(program2) {
|
|
13758
14089
|
const clients2 = program2.command("clients").description("Manage workspace clients (customers)");
|
|
13759
|
-
clients2.command("list").description(
|
|
14090
|
+
clients2.command("list").description(
|
|
14091
|
+
"List clients (newest first), with optional search, product and date-added filters"
|
|
14092
|
+
).option("--search <query>", "match email / first name / last name").option("--limit <n>", "page size (max 100)").option("--cursor <cursor>", "keyset cursor from a previous page").option("--with-products", "only clients with active product access").option("--without-products", "only clients with no product access").option(
|
|
14093
|
+
"--created-from <date>",
|
|
14094
|
+
"only clients added on or after this date (YYYY-MM-DD or ISO 8601 date-time)"
|
|
14095
|
+
).option(
|
|
14096
|
+
"--created-to <date>",
|
|
14097
|
+
"only clients added on or before this date (YYYY-MM-DD keeps the whole day)"
|
|
14098
|
+
).action(
|
|
13760
14099
|
async (options, command) => {
|
|
13761
14100
|
const flags = getGlobalFlags(command);
|
|
13762
14101
|
if (options.withProducts && options.withoutProducts) {
|
|
@@ -13767,7 +14106,9 @@ function registerClientsCommands(program2) {
|
|
|
13767
14106
|
search: options.search,
|
|
13768
14107
|
limit: options.limit !== void 0 ? parsePositiveInt4(options.limit, "--limit") : void 0,
|
|
13769
14108
|
cursor: options.cursor,
|
|
13770
|
-
products: options.withProducts ? "has" : options.withoutProducts ? "no" : void 0
|
|
14109
|
+
products: options.withProducts ? "has" : options.withoutProducts ? "no" : void 0,
|
|
14110
|
+
createdFrom: options.createdFrom,
|
|
14111
|
+
createdTo: options.createdTo
|
|
13771
14112
|
});
|
|
13772
14113
|
emit(flags, page, () => {
|
|
13773
14114
|
printTable(
|
|
@@ -13969,7 +14310,13 @@ Next page: --cursor ${page.nextCursor}
|
|
|
13969
14310
|
});
|
|
13970
14311
|
}
|
|
13971
14312
|
);
|
|
13972
|
-
clients2.command("grant").description("Grant a client access to a product without a purchase").argument("<productId>", "product id").requiredOption("--email <email>", "client email").option("--first-name <name>", "client first name").option("--last-name <name>", "client last name").option("--price <priceId>", "enroll under an existing price without billing").option("--plan <planId>", "plan to enroll in (multi-plan products)").option("--until <date>", "ISO date the access ends (free grants only); omit for lifetime").option("--welcome-email", "send the welcome/sign-in email (default: grant silently)").
|
|
14313
|
+
clients2.command("grant").description("Grant a client access to a product without a purchase").argument("<productId>", "product id").requiredOption("--email <email>", "client email").option("--first-name <name>", "client first name").option("--last-name <name>", "client last name").option("--price <priceId>", "enroll under an existing price without billing").option("--plan <planId>", "plan to enroll in (multi-plan products)").option("--until <date>", "ISO date the access ends (free grants only); omit for lifetime").option("--welcome-email", "send the welcome/sign-in email (default: grant silently)").option(
|
|
14314
|
+
"--run-purchase-automations",
|
|
14315
|
+
"also run the product's purchase automations, marked as a grant (default: off)"
|
|
14316
|
+
).option(
|
|
14317
|
+
"--paid <amount>",
|
|
14318
|
+
"the client already paid outside Zanfia (e.g. 99.50): records one paid order"
|
|
14319
|
+
).option("--currency <code>", "currency of --paid: pln, eur, usd or gbp").option("--paid-at <date>", "ISO date the client paid (default: now; not in the future)").option("--payment-method <method>", `how they paid: ${GRANT_PAYMENT_METHODS.join(" | ")}`).option("--payment-note <text>", "note on the payment, e.g. the proforma number (max 300)").action(
|
|
13973
14320
|
async (productId, options, command) => {
|
|
13974
14321
|
const flags = getGlobalFlags(command);
|
|
13975
14322
|
if (options.until !== void 0 && Number.isNaN(Date.parse(options.until))) {
|
|
@@ -13978,6 +14325,7 @@ Next page: --cursor ${page.nextCursor}
|
|
|
13978
14325
|
if (options.until !== void 0 && options.price !== void 0) {
|
|
13979
14326
|
throw new Error("--until applies to free grants only \u2014 omit --price.");
|
|
13980
14327
|
}
|
|
14328
|
+
const payment = parseGrantPayment(options);
|
|
13981
14329
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
13982
14330
|
const result = await client.grantClientAccess(productId, {
|
|
13983
14331
|
email: options.email,
|
|
@@ -13986,13 +14334,14 @@ Next page: --cursor ${page.nextCursor}
|
|
|
13986
14334
|
priceId: options.price,
|
|
13987
14335
|
planId: options.plan,
|
|
13988
14336
|
accessUntil: options.until,
|
|
13989
|
-
sendWelcomeEmail: options.welcomeEmail
|
|
14337
|
+
sendWelcomeEmail: options.welcomeEmail,
|
|
14338
|
+
runPurchaseAutomations: options.runPurchaseAutomations,
|
|
14339
|
+
...payment ? { payment } : {}
|
|
13990
14340
|
});
|
|
13991
14341
|
emit(flags, result, () => {
|
|
13992
14342
|
const kind = result.isFree ? "free" : "paid";
|
|
13993
14343
|
process.stdout.write(
|
|
13994
|
-
`Granted ${options.email} ${kind} access to product ${productId}
|
|
13995
|
-
`
|
|
14344
|
+
`Granted ${options.email} ${kind} access to product ${productId}` + (payment ? " and recorded the payment as a paid order" : "") + ".\n"
|
|
13996
14345
|
);
|
|
13997
14346
|
});
|
|
13998
14347
|
}
|
|
@@ -19297,9 +19646,14 @@ join again to refresh it.
|
|
|
19297
19646
|
// src/commands/liveRooms/index.ts
|
|
19298
19647
|
var minutes = (seconds) => `${Math.round(seconds / 60)} min`;
|
|
19299
19648
|
var percent = (part, whole) => whole > 0 ? `${Math.round(part / whole * 100)}%` : "-";
|
|
19649
|
+
var WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
19300
19650
|
function describeSchedule(room) {
|
|
19301
19651
|
if (!room.schedule) return "fixed";
|
|
19302
19652
|
const until = room.schedule.until ? ` until ${room.schedule.until}` : "";
|
|
19653
|
+
if (room.schedule.kind === "daily") {
|
|
19654
|
+
const days = room.schedule.weekdays ? room.schedule.weekdays.map((day) => WEEKDAYS[day - 1] ?? String(day)).join(", ") : "daily";
|
|
19655
|
+
return `${days} at ${room.schedule.times.join(", ")} (${room.timezone})${until}`;
|
|
19656
|
+
}
|
|
19303
19657
|
return `every ${room.schedule.everyMinutes} min${until}`;
|
|
19304
19658
|
}
|
|
19305
19659
|
function registerLiveRoomsCommands(program2) {
|
|
@@ -19606,7 +19960,7 @@ var NoopUrlStorage = /* @__PURE__ */ (function() {
|
|
|
19606
19960
|
}]);
|
|
19607
19961
|
})();
|
|
19608
19962
|
|
|
19609
|
-
// ../../node_modules/
|
|
19963
|
+
// ../../node_modules/js-base64/base64.mjs
|
|
19610
19964
|
var version = "3.7.8";
|
|
19611
19965
|
var VERSION = version;
|
|
19612
19966
|
var _hasBuffer = typeof Buffer === "function";
|
|
@@ -24729,6 +25083,7 @@ var PAGE_INTENTS = [
|
|
|
24729
25083
|
var SOURCE_SITE_MODES = ["rebuild", "inspired"];
|
|
24730
25084
|
var EVENT_MODES2 = ["live", "on_demand"];
|
|
24731
25085
|
var OPT_IN_MODES2 = ["single", "double"];
|
|
25086
|
+
var TRACKING_MODES = ["inherit", "selected", "none"];
|
|
24732
25087
|
var SCHEDULER_UNAVAILABLE_STATUS3 = 503;
|
|
24733
25088
|
function parseChoice5(value, allowed, flag) {
|
|
24734
25089
|
const match = allowed.find((candidate) => candidate === value);
|
|
@@ -24865,6 +25220,11 @@ function registerPagesCommands(program2) {
|
|
|
24865
25220
|
`);
|
|
24866
25221
|
process.stdout.write(` Cookie banner: ${page.hideCookieBanner ? "hidden" : "shown"}
|
|
24867
25222
|
`);
|
|
25223
|
+
const trackingIds = page.tracking?.integrationIds ?? [];
|
|
25224
|
+
process.stdout.write(
|
|
25225
|
+
` Tracking: ${page.tracking?.mode ?? "inherit"}${trackingIds.length > 0 ? ` (${trackingIds.join(", ")})` : ""}
|
|
25226
|
+
`
|
|
25227
|
+
);
|
|
24868
25228
|
});
|
|
24869
25229
|
});
|
|
24870
25230
|
pages.command("create").description("Create a hosted page from an HTML file or inline HTML").requiredOption("--slug <slug>", "public URL slug (served at /page/<slug>)").requiredOption("--title <title>", "page title").option("--file <path>", "path to an .html file to host").option("--html <html>", "inline HTML body (alternative to --file)").option("--publish", "publish immediately instead of saving as draft").action(async (options, command) => {
|
|
@@ -24886,17 +25246,33 @@ function registerPagesCommands(program2) {
|
|
|
24886
25246
|
`);
|
|
24887
25247
|
});
|
|
24888
25248
|
});
|
|
24889
|
-
pages.command("update").description(
|
|
25249
|
+
pages.command("update").description(
|
|
25250
|
+
"Update a page (slug, title, HTML body, cookie-banner visibility, or its tracking tools)"
|
|
25251
|
+
).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(
|
|
24890
25252
|
"--no-cookie-banner",
|
|
24891
25253
|
"hide the workspace cookie banner on this page (pixels still wait for consent given elsewhere)"
|
|
25254
|
+
).option(
|
|
25255
|
+
"--tracking <mode>",
|
|
25256
|
+
"tracking tools on this page: inherit (workspace defaults), selected (--tracking-integrations) or none"
|
|
25257
|
+
).option(
|
|
25258
|
+
"--tracking-integrations <ids>",
|
|
25259
|
+
"comma-separated tracking integration ids for --tracking selected (one per platform)"
|
|
24892
25260
|
).action(async (id, options, command) => {
|
|
24893
25261
|
const flags = getGlobalFlags(command);
|
|
24894
25262
|
const html = await resolveHtml2(options);
|
|
25263
|
+
const trackingIds = options.trackingIntegrations?.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
|
|
25264
|
+
const trackingMode = options.tracking !== void 0 ? parseChoice5(options.tracking, TRACKING_MODES, "--tracking") : trackingIds !== void 0 ? "selected" : void 0;
|
|
24895
25265
|
const input = {
|
|
24896
25266
|
slug: options.slug,
|
|
24897
25267
|
title: options.title,
|
|
24898
25268
|
html,
|
|
24899
|
-
...options.cookieBanner !== void 0 ? { hideCookieBanner: !options.cookieBanner } : {}
|
|
25269
|
+
...options.cookieBanner !== void 0 ? { hideCookieBanner: !options.cookieBanner } : {},
|
|
25270
|
+
...trackingMode !== void 0 ? {
|
|
25271
|
+
tracking: {
|
|
25272
|
+
mode: trackingMode,
|
|
25273
|
+
...trackingIds !== void 0 ? { integrationIds: trackingIds } : {}
|
|
25274
|
+
}
|
|
25275
|
+
} : {}
|
|
24900
25276
|
};
|
|
24901
25277
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
24902
25278
|
const result = await client.updatePage(id, input);
|
|
@@ -25298,6 +25674,10 @@ function registerProductsCommands(program2) {
|
|
|
25298
25674
|
`);
|
|
25299
25675
|
process.stdout.write(` Status: ${product.status}
|
|
25300
25676
|
`);
|
|
25677
|
+
if (product.storefrontVisibility) {
|
|
25678
|
+
process.stdout.write(` Visibility: ${product.storefrontVisibility}
|
|
25679
|
+
`);
|
|
25680
|
+
}
|
|
25301
25681
|
process.stdout.write(` Clients: ${product.clientCount}
|
|
25302
25682
|
`);
|
|
25303
25683
|
if (product.description) {
|
|
@@ -25384,13 +25764,21 @@ function registerProductsCommands(program2) {
|
|
|
25384
25764
|
});
|
|
25385
25765
|
}
|
|
25386
25766
|
);
|
|
25387
|
-
products2.command("update").description("Update a product's name and/or
|
|
25767
|
+
products2.command("update").description("Update a product's name, description and/or storefront visibility").argument("<id>", "product id").option("--name <name>", "new product name").option("--description <description>", "new product description").option(
|
|
25768
|
+
"--visibility <public|unlisted|private>",
|
|
25769
|
+
"where buyers find it once published: public (store + product page), unlisted (product page by link only), private (checkout links only)",
|
|
25770
|
+
parseStorefrontVisibility
|
|
25771
|
+
).action(
|
|
25388
25772
|
async (id, options, command) => {
|
|
25773
|
+
if (options.name === void 0 && options.description === void 0 && options.visibility === void 0) {
|
|
25774
|
+
throw new Error("Nothing to update: pass --name, --description and/or --visibility.");
|
|
25775
|
+
}
|
|
25389
25776
|
const flags = getGlobalFlags(command);
|
|
25390
25777
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25391
25778
|
const result = await client.updateProduct(id, {
|
|
25392
25779
|
name: options.name,
|
|
25393
|
-
description: options.description
|
|
25780
|
+
description: options.description,
|
|
25781
|
+
storefrontVisibility: options.visibility
|
|
25394
25782
|
});
|
|
25395
25783
|
emit(flags, result, () => {
|
|
25396
25784
|
process.stdout.write(`Updated product ${result.productId}
|
|
@@ -25407,6 +25795,26 @@ function registerProductsCommands(program2) {
|
|
|
25407
25795
|
`);
|
|
25408
25796
|
});
|
|
25409
25797
|
});
|
|
25798
|
+
products2.command("delete").description(
|
|
25799
|
+
"Delete a product (soft-delete, like the dashboard): offers archived, customers lose access, subscriptions cancelled without refunds, data purged after 30 days. Requires --confirm."
|
|
25800
|
+
).argument("<id>", "product id").option("--confirm", "required \u2014 acknowledge that customers lose access and subscriptions end").action(async (id, options, command) => {
|
|
25801
|
+
requireConfirm(options.confirm, `delete product ${id}`);
|
|
25802
|
+
const flags = getGlobalFlags(command);
|
|
25803
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
25804
|
+
const result = await client.deleteProduct(id);
|
|
25805
|
+
emit(flags, result, () => {
|
|
25806
|
+
process.stdout.write(
|
|
25807
|
+
`Deleted product ${result.productId}; data is purged after ${new Date(result.purgeAfter * 1e3).toISOString().slice(0, 10)}.
|
|
25808
|
+
`
|
|
25809
|
+
);
|
|
25810
|
+
if (result.activeGrants > 0) {
|
|
25811
|
+
process.stdout.write(
|
|
25812
|
+
`${result.activeGrants} customer(s) lost access; ${result.cancellableSubscriptions} subscription(s) are being cancelled (run ${result.runId ?? "n/a"}).
|
|
25813
|
+
`
|
|
25814
|
+
);
|
|
25815
|
+
}
|
|
25816
|
+
});
|
|
25817
|
+
});
|
|
25410
25818
|
products2.command("publish").description("Publish a product (status \u2192 published; live for customers)").argument("<id>", "product id").action(async (id, _, command) => {
|
|
25411
25819
|
const flags = getGlobalFlags(command);
|
|
25412
25820
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
@@ -25951,6 +26359,18 @@ function registerProductsCommands(program2) {
|
|
|
25951
26359
|
);
|
|
25952
26360
|
});
|
|
25953
26361
|
});
|
|
26362
|
+
files.command("rename").description(
|
|
26363
|
+
"Rename a file in place (its name is the buyer\u2019s download filename; order and uid stay)"
|
|
26364
|
+
).argument("<uid>", "file uid (see `products files get`)").requiredOption("--product <id>", "downloads product id").requiredOption("--name <name>", 'new file name, e.g. "Tom 1.epub" (max 255 characters)').action(async (uid, options, command) => {
|
|
26365
|
+
const flags = getGlobalFlags(command);
|
|
26366
|
+
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
26367
|
+
const result = await client.renameDownloadFile(options.product, uid, options.name);
|
|
26368
|
+
emit(flags, result, () => {
|
|
26369
|
+
process.stdout.write(`Renamed ${result.file.uid} to "${result.file.name}".
|
|
26370
|
+
`);
|
|
26371
|
+
printFilesTable(result.files);
|
|
26372
|
+
});
|
|
26373
|
+
});
|
|
25954
26374
|
files.command("reorder").description("Reorder the file list \u2014 pass EVERY current uid in the new order").argument("<uid...>", "the full uid permutation, in the new order").requiredOption("--product <id>", "downloads product id").action(async (uids, options, command) => {
|
|
25955
26375
|
const flags = getGlobalFlags(command);
|
|
25956
26376
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
@@ -26749,6 +27169,8 @@ function describeCondition(condition) {
|
|
|
26749
27169
|
return condition.operator === "has" ? "has a phone number" : "has no phone number";
|
|
26750
27170
|
case "campaign":
|
|
26751
27171
|
return `${condition.operator.replace("-", " ")} campaign ${condition.campaignId}`;
|
|
27172
|
+
case "email":
|
|
27173
|
+
return `email ${condition.operator === "is-any" ? "is one of" : "is none of"} ${condition.emails.join(", ")} (set in the dashboard)`;
|
|
26752
27174
|
}
|
|
26753
27175
|
}
|
|
26754
27176
|
function registerSegmentsCommands(program2) {
|
|
@@ -27123,15 +27545,25 @@ function registerSiteCommands(program2) {
|
|
|
27123
27545
|
);
|
|
27124
27546
|
update.command("general").description(
|
|
27125
27547
|
"Page title/description, language, showPageTitle, mainPage* (undefined = untouched)"
|
|
27126
|
-
).option("--title <text>", "browser/page title of the site").option("--description <text>", "
|
|
27548
|
+
).option("--title <text>", "browser/page title of the site").option("--description <text>", "short description (storefront header + meta fallback)").option(
|
|
27549
|
+
"--seo-description <text>",
|
|
27550
|
+
"meta description for search engines (empty = short description)"
|
|
27551
|
+
).option(
|
|
27552
|
+
"--robots-rules <text>",
|
|
27553
|
+
String.raw`extra robots.txt lines (User-agent / Allow / Disallow / Crawl-delay); separate lines with \n, or use --json`
|
|
27554
|
+
).option("--language <pl|en|default>", "UI language of the public site").option("--show-page-title <on|off>", "render the page title in the header").option(
|
|
27127
27555
|
"--json <fileOrJson>",
|
|
27128
|
-
"inline JSON object or .json path \u2014 pageTitle, pageDescription, showPageTitle, language, mainPage, mainPageId, mainPageCommunityId"
|
|
27556
|
+
"inline JSON object or .json path \u2014 pageTitle, pageDescription, seoDescription, robotsTxtRules, showPageTitle, language, mainPage, mainPageId, mainPageCommunityId"
|
|
27129
27557
|
).action(
|
|
27130
27558
|
async (options, command) => {
|
|
27131
27559
|
const flags = getGlobalFlags(command);
|
|
27132
27560
|
const body = baseBody(options.json);
|
|
27133
27561
|
if (options.title !== void 0) body.pageTitle = options.title;
|
|
27134
27562
|
if (options.description !== void 0) body.pageDescription = options.description;
|
|
27563
|
+
if (options.seoDescription !== void 0) body.seoDescription = options.seoDescription;
|
|
27564
|
+
if (options.robotsRules !== void 0) {
|
|
27565
|
+
body.robotsTxtRules = options.robotsRules.split(String.raw`\n`).join("\n");
|
|
27566
|
+
}
|
|
27135
27567
|
if (options.language !== void 0) {
|
|
27136
27568
|
body.language = parseChoice7(options.language, PLATFORM_LANGUAGES, "--language");
|
|
27137
27569
|
}
|
|
@@ -28010,7 +28442,8 @@ var CONVERSION_EVENT_STATUSES = [
|
|
|
28010
28442
|
"sent",
|
|
28011
28443
|
"delivered",
|
|
28012
28444
|
"failed",
|
|
28013
|
-
"consent_blocked"
|
|
28445
|
+
"consent_blocked",
|
|
28446
|
+
"test_skipped"
|
|
28014
28447
|
];
|
|
28015
28448
|
var CONVERSION_TRACKING_CLIENTS = [
|
|
28016
28449
|
"metaConversionApi",
|
|
@@ -28113,12 +28546,12 @@ function registerStatsCommands(program2) {
|
|
|
28113
28546
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
28114
28547
|
const report = await client.statsSales(range);
|
|
28115
28548
|
emit(flags, report, () => {
|
|
28116
|
-
const c = report.
|
|
28549
|
+
const c = report.currency;
|
|
28117
28550
|
process.stdout.write(
|
|
28118
28551
|
`Sales (${report.period.from} \u2192 ${report.period.to}), currency ${c}
|
|
28119
|
-
Net revenue: ${money(report.
|
|
28552
|
+
Net revenue: ${money(report.totalRevenueCents)} ${c}
|
|
28120
28553
|
Transactions: ${report.transactions} Refunds: ${report.refunds}
|
|
28121
|
-
AOV: ${money(report.
|
|
28554
|
+
AOV: ${money(report.averageOrderValueCents)} ${c} New subscribers: ${report.newSubscribers}
|
|
28122
28555
|
|
|
28123
28556
|
`
|
|
28124
28557
|
);
|
|
@@ -28128,7 +28561,7 @@ function registerStatsCommands(program2) {
|
|
|
28128
28561
|
report.perProduct.map((r) => ({
|
|
28129
28562
|
id: r.id,
|
|
28130
28563
|
name: r.name,
|
|
28131
|
-
revenue: money(r.
|
|
28564
|
+
revenue: money(r.revenueCents),
|
|
28132
28565
|
transactions: r.transactions,
|
|
28133
28566
|
refunds: r.refunds
|
|
28134
28567
|
})),
|
|
@@ -28142,11 +28575,11 @@ function registerStatsCommands(program2) {
|
|
|
28142
28575
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
28143
28576
|
const report = await client.statsRecurring(options.currency);
|
|
28144
28577
|
emit(flags, report, () => {
|
|
28145
|
-
const c = report.
|
|
28578
|
+
const c = report.currency;
|
|
28146
28579
|
process.stdout.write(
|
|
28147
28580
|
`Recurring revenue (currency ${c})
|
|
28148
|
-
MRR: ${money(report.
|
|
28149
|
-
Active subscriptions: ${report.
|
|
28581
|
+
MRR: ${money(report.mrrCents)} ${c} ARR: ${money(report.arrCents)} ${c}
|
|
28582
|
+
Active subscriptions: ${report.activeSubscriptions} Ending soon: ${report.endingSoonCount}
|
|
28150
28583
|
|
|
28151
28584
|
`
|
|
28152
28585
|
);
|
|
@@ -28155,8 +28588,8 @@ function registerStatsCommands(program2) {
|
|
|
28155
28588
|
report.perProduct.map((r) => ({
|
|
28156
28589
|
id: r.productId,
|
|
28157
28590
|
name: r.productName,
|
|
28158
|
-
mrr: money(r.
|
|
28159
|
-
subs: r.
|
|
28591
|
+
mrr: money(r.mrrCents),
|
|
28592
|
+
subs: r.activeSubscriptions
|
|
28160
28593
|
})),
|
|
28161
28594
|
["id", "name", "mrr", "subs"]
|
|
28162
28595
|
);
|
|
@@ -28171,13 +28604,13 @@ function registerStatsCommands(program2) {
|
|
|
28171
28604
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
28172
28605
|
const report = await client.statsMrrHistory(range);
|
|
28173
28606
|
emit(flags, report, () => {
|
|
28174
|
-
process.stdout.write(`MRR history (currency ${report.
|
|
28607
|
+
process.stdout.write(`MRR history (currency ${report.currency})
|
|
28175
28608
|
`);
|
|
28176
28609
|
printTable(
|
|
28177
28610
|
report.points.map((p) => ({
|
|
28178
28611
|
date: p.date,
|
|
28179
28612
|
mrr: money(p.mrrCents),
|
|
28180
|
-
subs: p.
|
|
28613
|
+
subs: p.activeSubscriptions,
|
|
28181
28614
|
new: money(p.newMrrCents),
|
|
28182
28615
|
churned: money(p.churnedMrrCents),
|
|
28183
28616
|
churnRate: `${(p.churnRate * 100).toFixed(1)}%`
|
|
@@ -28257,11 +28690,23 @@ function registerStatsCommands(program2) {
|
|
|
28257
28690
|
checkout: s.checkoutId,
|
|
28258
28691
|
stage: s.furthestStage,
|
|
28259
28692
|
status: s.status,
|
|
28260
|
-
|
|
28261
|
-
source: s.source ?? "",
|
|
28693
|
+
converted: s.converted ? "yes" : "",
|
|
28694
|
+
source: s.source.utmSource ?? s.source.referrer ?? "",
|
|
28695
|
+
country: s.country ?? "",
|
|
28696
|
+
declined: s.paymentFailure?.reason ?? "",
|
|
28262
28697
|
startedAt: s.startedAt
|
|
28263
28698
|
})),
|
|
28264
|
-
[
|
|
28699
|
+
[
|
|
28700
|
+
"id",
|
|
28701
|
+
"checkout",
|
|
28702
|
+
"stage",
|
|
28703
|
+
"status",
|
|
28704
|
+
"converted",
|
|
28705
|
+
"source",
|
|
28706
|
+
"country",
|
|
28707
|
+
"declined",
|
|
28708
|
+
"startedAt"
|
|
28709
|
+
]
|
|
28265
28710
|
);
|
|
28266
28711
|
});
|
|
28267
28712
|
});
|
|
@@ -28270,7 +28715,7 @@ function registerStatsCommands(program2) {
|
|
|
28270
28715
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
28271
28716
|
const report = await client.statsProduct(productId, options.currency);
|
|
28272
28717
|
emit(flags, report, () => {
|
|
28273
|
-
const c = report.
|
|
28718
|
+
const c = report.currency;
|
|
28274
28719
|
process.stdout.write(
|
|
28275
28720
|
`Product ${report.productId} (${report.window.startDate} \u2192 ${report.window.endDate}), currency ${c}
|
|
28276
28721
|
Revenue: ${money(report.revenue.totalCents)} ${c} (prev ${money(report.revenue.previousTotalCents)} ${c})
|
|
@@ -29440,17 +29885,17 @@ var GRAPH_NODE_KINDS = /* @__PURE__ */ new Set([
|
|
|
29440
29885
|
]);
|
|
29441
29886
|
var GRAPH_CONDITION_BRANCHES = ["true", "false"];
|
|
29442
29887
|
var EXCERPT_LENGTH = 120;
|
|
29443
|
-
function
|
|
29888
|
+
function isRecord3(value) {
|
|
29444
29889
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
29445
29890
|
}
|
|
29446
29891
|
function optionalString(value) {
|
|
29447
29892
|
return typeof value === "string" ? value : void 0;
|
|
29448
29893
|
}
|
|
29449
29894
|
function asAutomationGraph(raw) {
|
|
29450
|
-
if (!
|
|
29895
|
+
if (!isRecord3(raw) || !Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) return null;
|
|
29451
29896
|
const nodes = [];
|
|
29452
29897
|
for (const candidate of raw.nodes) {
|
|
29453
|
-
if (!
|
|
29898
|
+
if (!isRecord3(candidate)) continue;
|
|
29454
29899
|
const { id, kind, config, name, disabled } = candidate;
|
|
29455
29900
|
if (typeof id !== "string" || typeof kind !== "string" || !GRAPH_NODE_KINDS.has(kind)) {
|
|
29456
29901
|
continue;
|
|
@@ -29458,14 +29903,14 @@ function asAutomationGraph(raw) {
|
|
|
29458
29903
|
nodes.push({
|
|
29459
29904
|
id,
|
|
29460
29905
|
kind,
|
|
29461
|
-
config:
|
|
29906
|
+
config: isRecord3(config) ? config : {},
|
|
29462
29907
|
...typeof name === "string" && name.length > 0 ? { name } : {},
|
|
29463
29908
|
...disabled === true ? { disabled: true } : {}
|
|
29464
29909
|
});
|
|
29465
29910
|
}
|
|
29466
29911
|
const edges = [];
|
|
29467
29912
|
for (const candidate of raw.edges) {
|
|
29468
|
-
if (!
|
|
29913
|
+
if (!isRecord3(candidate)) continue;
|
|
29469
29914
|
const { source, target, sourceHandle } = candidate;
|
|
29470
29915
|
if (typeof source !== "string" || typeof target !== "string") continue;
|
|
29471
29916
|
const branch = GRAPH_CONDITION_BRANCHES.find((b) => b === sourceHandle);
|
|
@@ -29475,7 +29920,7 @@ function asAutomationGraph(raw) {
|
|
|
29475
29920
|
}
|
|
29476
29921
|
function emailConfigOf(node) {
|
|
29477
29922
|
const raw = node.config.email;
|
|
29478
|
-
if (!
|
|
29923
|
+
if (!isRecord3(raw)) return null;
|
|
29479
29924
|
return {
|
|
29480
29925
|
templateId: optionalString(raw.templateId) ?? null,
|
|
29481
29926
|
templateName: optionalString(raw.templateName) ?? null,
|
|
@@ -29627,7 +30072,7 @@ function isKnownActionType(value) {
|
|
|
29627
30072
|
function describeAction(node, templateNames) {
|
|
29628
30073
|
const { taskType } = node.config;
|
|
29629
30074
|
if (!isKnownActionType(taskType)) {
|
|
29630
|
-
const inner =
|
|
30075
|
+
const inner = isRecord3(node.config.action) ? optionalString(node.config.action.type) : void 0;
|
|
29631
30076
|
return `${typeof taskType === "string" ? taskType : "action"}${inner ? ` ${inner}` : ""}`;
|
|
29632
30077
|
}
|
|
29633
30078
|
const label = GRAPH_ACTION_LABELS[taskType];
|
|
@@ -29635,39 +30080,39 @@ function describeAction(node, templateNames) {
|
|
|
29635
30080
|
case "sendEmail":
|
|
29636
30081
|
return `${label} \u2014 ${describeEmail(node, templateNames)}`;
|
|
29637
30082
|
case "notifyOwner": {
|
|
29638
|
-
const owner =
|
|
30083
|
+
const owner = isRecord3(node.config.ownerNotification) ? node.config.ownerNotification : {};
|
|
29639
30084
|
return `${label} \u2014 subject: ${quote(optionalString(owner.subject))} \u2014 body: ${quote(excerptHtml(optionalString(owner.html)))}`;
|
|
29640
30085
|
}
|
|
29641
30086
|
case "sendSms": {
|
|
29642
|
-
const sms =
|
|
30087
|
+
const sms = isRecord3(node.config.sms) ? node.config.sms : {};
|
|
29643
30088
|
return `${label} \u2014 text: ${quote(excerptText(optionalString(sms.text)))}`;
|
|
29644
30089
|
}
|
|
29645
30090
|
case "webhook": {
|
|
29646
|
-
const webhook =
|
|
30091
|
+
const webhook = isRecord3(node.config.webhook) ? node.config.webhook : {};
|
|
29647
30092
|
const method = optionalString(webhook.method) ?? optionalString(webhook.type);
|
|
29648
30093
|
return `${label} \u2014 ${method ? `${method.toUpperCase()} ` : ""}${optionalString(webhook.url) ?? "(no url)"}`;
|
|
29649
30094
|
}
|
|
29650
30095
|
case "publishSocialPost": {
|
|
29651
|
-
const social =
|
|
30096
|
+
const social = isRecord3(node.config.social) ? node.config.social : {};
|
|
29652
30097
|
const targets = Array.isArray(social.targets) ? social.targets.join(", ") : "";
|
|
29653
30098
|
return `${label}${optionalString(social.mode) ? ` (${social.mode})` : ""}${targets ? ` to ${targets}` : ""} \u2014 text: ${quote(excerptText(optionalString(social.text)))}`;
|
|
29654
30099
|
}
|
|
29655
30100
|
case "createCommunityPost": {
|
|
29656
|
-
const post =
|
|
30101
|
+
const post = isRecord3(node.config.communityPost) ? node.config.communityPost : {};
|
|
29657
30102
|
const title = optionalString(post.title);
|
|
29658
30103
|
return `${label} in channel ${optionalString(post.channelId) ?? "?"}${title ? ` \u2014 title: ${quote(title)}` : ""} \u2014 text: ${quote(excerptText(optionalString(post.text)))}`;
|
|
29659
30104
|
}
|
|
29660
30105
|
case "updateClientTags": {
|
|
29661
|
-
const tags =
|
|
30106
|
+
const tags = isRecord3(node.config.clientTags) ? node.config.clientTags : {};
|
|
29662
30107
|
const ids = Array.isArray(tags.tagIds) ? tags.tagIds.join(", ") : "";
|
|
29663
30108
|
return `${label} \u2014 ${optionalString(tags.mode) ?? "update"} ${ids || "(no tags)"}`;
|
|
29664
30109
|
}
|
|
29665
30110
|
case "setEmailSubscription": {
|
|
29666
|
-
const sub =
|
|
30111
|
+
const sub = isRecord3(node.config.emailSubscription) ? node.config.emailSubscription : {};
|
|
29667
30112
|
return `${label} \u2014 ${optionalString(sub.status) ?? "?"}`;
|
|
29668
30113
|
}
|
|
29669
30114
|
case "openClientDeadline": {
|
|
29670
|
-
const deadline =
|
|
30115
|
+
const deadline = isRecord3(node.config.clientDeadline) ? node.config.clientDeadline : {};
|
|
29671
30116
|
const amount = typeof deadline.amount === "number" ? String(deadline.amount) : "?";
|
|
29672
30117
|
const endTime = optionalString(deadline.endTime);
|
|
29673
30118
|
return `${label} \u2014 ${optionalString(deadline.key) ?? "?"} for ${amount} ${optionalString(deadline.unit) ?? "?"}${endTime ? `, closes at ${endTime}` : ""}`;
|
|
@@ -29686,7 +30131,7 @@ function describeEmail(node, templateNames) {
|
|
|
29686
30131
|
return `subject: ${quote(email.subject)} \u2014 body: ${quote(excerptHtml(email.html))}`;
|
|
29687
30132
|
}
|
|
29688
30133
|
function describeCondition2(config) {
|
|
29689
|
-
const rules = Array.isArray(config.rules) ? config.rules.filter(
|
|
30134
|
+
const rules = Array.isArray(config.rules) ? config.rules.filter(isRecord3) : [];
|
|
29690
30135
|
if (rules.length === 0) return "(no rules)";
|
|
29691
30136
|
return rules.map(describeRule).join(" AND ");
|
|
29692
30137
|
}
|
|
@@ -29743,6 +30188,40 @@ function excerptText(text) {
|
|
|
29743
30188
|
}
|
|
29744
30189
|
|
|
29745
30190
|
// src/commands/workspace/index.ts
|
|
30191
|
+
function toLoginRow(profile) {
|
|
30192
|
+
const { credentials } = profile;
|
|
30193
|
+
return {
|
|
30194
|
+
key: profile.key,
|
|
30195
|
+
active: profile.active,
|
|
30196
|
+
workspaceId: credentials.workspaceId ?? null,
|
|
30197
|
+
workspaceName: credentials.workspaceName ?? null,
|
|
30198
|
+
workspaceSlug: credentials.workspaceSlug ?? null,
|
|
30199
|
+
kind: credentials.kind ?? "workspace",
|
|
30200
|
+
userEmail: credentials.userEmail ?? null,
|
|
30201
|
+
source: credentials.source ?? "manual",
|
|
30202
|
+
expiresAt: credentials.expiresAt ?? null
|
|
30203
|
+
};
|
|
30204
|
+
}
|
|
30205
|
+
function printLogins(profiles) {
|
|
30206
|
+
if (profiles.length === 0) {
|
|
30207
|
+
process.stdout.write("No stored logins. Run `zanfia login` (add --workspace <id|slug>).\n");
|
|
30208
|
+
return;
|
|
30209
|
+
}
|
|
30210
|
+
for (const profile of profiles) {
|
|
30211
|
+
const row = toLoginRow(profile);
|
|
30212
|
+
const name = row.workspaceName ?? "(unnamed)";
|
|
30213
|
+
const slug = row.workspaceSlug ? ` [${row.workspaceSlug}]` : "";
|
|
30214
|
+
const member = row.kind === "member" ? " (community member)" : "";
|
|
30215
|
+
const expires = row.expiresAt ? `, expires ${row.expiresAt.slice(0, 10)}` : "";
|
|
30216
|
+
process.stdout.write(
|
|
30217
|
+
`${row.active ? "*" : " "} ${name}${slug}${member} - ${row.workspaceId ?? row.key}${row.userEmail ? `, ${row.userEmail}` : ""}${expires}
|
|
30218
|
+
`
|
|
30219
|
+
);
|
|
30220
|
+
}
|
|
30221
|
+
process.stdout.write(
|
|
30222
|
+
"\n* = active. Switch: `zanfia workspace use <id|slug>`; one command: `--workspace <id|slug>`.\n"
|
|
30223
|
+
);
|
|
30224
|
+
}
|
|
29746
30225
|
function parseOnOff5(value, flag) {
|
|
29747
30226
|
const match = ["on", "off"].find((v) => v === value);
|
|
29748
30227
|
if (!match) throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
|
|
@@ -29784,8 +30263,23 @@ function printSettings(settings) {
|
|
|
29784
30263
|
}
|
|
29785
30264
|
function registerWorkspaceCommands(program2) {
|
|
29786
30265
|
const workspace2 = program2.command("workspace").description(
|
|
29787
|
-
|
|
30266
|
+
"Workspace-level settings (the name, the scheduling timezone and the video transcription defaults) and the workspace logins stored on this device (list, use)"
|
|
29788
30267
|
);
|
|
30268
|
+
workspace2.command("list").description("List the workspace logins stored on this device (* = active)").action((_options, command) => {
|
|
30269
|
+
const flags = getGlobalFlags(command);
|
|
30270
|
+
const profiles = listStoredProfiles();
|
|
30271
|
+
emit(flags, profiles.map(toLoginRow), () => printLogins(profiles));
|
|
30272
|
+
});
|
|
30273
|
+
workspace2.command("use").description("Make another stored workspace login the active one (no new login)").argument("<id|slug>", "workspace id, site slug or name of a stored login").action((ref, _options, command) => {
|
|
30274
|
+
const flags = getGlobalFlags(command);
|
|
30275
|
+
const profile = useStoredProfile(ref);
|
|
30276
|
+
emit(flags, toLoginRow(profile), () => {
|
|
30277
|
+
process.stdout.write(
|
|
30278
|
+
`Active workspace: ${profile.credentials.workspaceName ?? "?"} (${profile.credentials.workspaceId ?? profile.key})
|
|
30279
|
+
`
|
|
30280
|
+
);
|
|
30281
|
+
});
|
|
30282
|
+
});
|
|
29789
30283
|
workspace2.command("get", { isDefault: true }).description("Show the workspace settings").action(async (_options, command) => {
|
|
29790
30284
|
const flags = getGlobalFlags(command);
|
|
29791
30285
|
const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
|
|
@@ -29882,8 +30376,8 @@ function applyShortHelp(program2) {
|
|
|
29882
30376
|
|
|
29883
30377
|
// src/program.ts
|
|
29884
30378
|
function resolveCliVersion() {
|
|
29885
|
-
if ("0.
|
|
29886
|
-
return "0.
|
|
30379
|
+
if ("0.9.0".length > 0) {
|
|
30380
|
+
return "0.9.0";
|
|
29887
30381
|
}
|
|
29888
30382
|
try {
|
|
29889
30383
|
const raw = readFileSync28(new URL("../package.json", import.meta.url), "utf8");
|
|
@@ -29924,7 +30418,7 @@ Signing in \u2014 two roles, two consent pages:
|
|
|
29924
30418
|
registerLoginCommand(program2);
|
|
29925
30419
|
registerRegisterCommand(program2);
|
|
29926
30420
|
registerJoinCommand(program2);
|
|
29927
|
-
registerLogoutCommand(program2
|
|
30421
|
+
registerLogoutCommand(program2);
|
|
29928
30422
|
registerAuthCommands(program2);
|
|
29929
30423
|
registerProductsCommands(program2);
|
|
29930
30424
|
registerOffersCommands(program2);
|