@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.
Files changed (3) hide show
  1. package/README.md +52 -52
  2. package/dist/index.js +673 -179
  3. 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/combine-errors/node_modules/lodash.uniqby/index.js
5252
+ // ../../node_modules/lodash.uniqby/index.js
5253
5253
  var require_lodash8 = __commonJS({
5254
- "../../node_modules/combine-errors/node_modules/lodash.uniqby/index.js"(exports, module) {
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 CONFIG_DIR = join(homedir(), ".config", "zanfia");
6911
- var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
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
- function readCredentials() {
6933
- try {
6934
- const raw = readFileSync(CREDENTIALS_FILE, "utf8");
6935
- const parsed = JSON.parse(raw);
6936
- if (parsed !== null && typeof parsed === "object" && "apiKey" in parsed && typeof parsed.apiKey === "string") {
6937
- return parsed;
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
- return null;
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 null;
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
- mkdirSync(dirname(CREDENTIALS_FILE), { recursive: true, mode: 448 });
6946
- writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
6947
- mode: 384
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
- return CREDENTIALS_FILE;
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 clearCredentials() {
6999
- try {
7000
- rmSync(CREDENTIALS_FILE, { force: true });
7001
- return true;
7002
- } catch {
7003
- return false;
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 isRecord(value) {
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 (!isRecord(raw)) continue;
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 (!isRecord(result)) return [];
10274
+ if (!isRecord2(result)) return [];
10136
10275
  const rows = result[key];
10137
10276
  if (!Array.isArray(rows)) return [];
10138
- return rows.filter(isRecord);
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 course-completion certificates of the workspace, newest first, optionally filtered to one course product (`productId`); `limit` caps the rows (service default applies). Each row is the credential: public `certificateId` (`cert_\u2026`, the id every certificate write takes), recipient name/email, course 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."
10192
- ).option("--product <value>", "Course product id \u2014 omit for all courses.").option(
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 course-completion certificates to the listed clients of ONE course product. The call validates the batch (404 unknown product or `templateId`, 400 when the product is not a course), skips clients who already hold a certificate for it (one certificate per client per course, 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). `clientIds` is capped at 1000 and de-duplicated. Every issuance EMAILS the student \u2014 the emails cannot be recalled.'
10210
- ).argument("<productId>", "Course product id (must be a course).").requiredOption(
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
- "Skip clients who have not completed the course (checked per client at issuance time). Default false = issue to everyone listed."
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 course template, else the workspace default."
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.").action(async (options, command) => {
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
- process.stdout.write(`Credentials file: ${credentialsPath}
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(urls.loginUrl, {
11501
- redirect_uri: redirectUri,
11502
- state,
11503
- code_challenge: challenge,
11504
- code_challenge_method: "S256",
11505
- hostname: host,
11506
- client: "zanfia-cli"
11507
- });
11508
- process.stdout.write(`Opening ${consentUrl}
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(consentUrl);
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(`Workspace: ${exchanged.workspaceName} (${exchanged.workspaceId})
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 gets a workspace token.
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, clear) {
11690
- auth.command("logout").description("Forget the stored CLI credentials").action(() => {
11691
- const cleared = clear();
11692
- if (cleared) {
11693
- process.stdout.write(`Removed credentials at ${credentialsPath}.
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 Error(
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 via an emailed code and log the CLI in").option("--email <email>", "account email (prompted when omitted)").option("--name <fullName>", "your full name (prompted when omitted)").option(
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
- The same email-code registration as the Zanfia dashboard, entirely in the
11988
- terminal: you get a 6-digit code by email, the account plus its personal
11989
- workspace are created, and the CLI ends up logged in exactly as if you had
11990
- run \`login\` \u2014 no browser needed.
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("List clients (newest first), with optional search and product filter").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").action(
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)").action(
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/tus-js-client/node_modules/js-base64/base64.mjs
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("Update a page (slug, title, HTML body, or cookie-banner visibility)").argument("<id>", "page id").option("--slug <slug>", "new slug").option("--title <title>", "new title").option("--file <path>", "path to a new .html file").option("--html <html>", "new inline HTML body").option("--cookie-banner", "show the workspace cookie banner on this page again").option(
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 description").argument("<id>", "product id").option("--name <name>", "new product name").option("--description <description>", "new product description").action(
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>", "meta description of the site").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(
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.primaryCurrency;
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.primaryNetRevenueCents)} ${c}
28552
+ Net revenue: ${money(report.totalRevenueCents)} ${c}
28120
28553
  Transactions: ${report.transactions} Refunds: ${report.refunds}
28121
- AOV: ${money(report.aovCents)} ${c} New subscribers: ${report.totalNewSubscribers}
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.primaryRevenueCents),
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.primaryCurrency;
28578
+ const c = report.currency;
28146
28579
  process.stdout.write(
28147
28580
  `Recurring revenue (currency ${c})
28148
- MRR: ${money(report.primaryMrrCents)} ${c} ARR: ${money(report.primaryArrCents)} ${c}
28149
- Active subscriptions: ${report.activeRecurringSubscriptions} Ending soon: ${report.endingSoonCount}
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.primaryMrrCents),
28159
- subs: r.activeRecurringSubscriptions
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.primaryCurrency})
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.activeRecurringSubscriptions,
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
- visitor: s.visitorLabel,
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
- ["id", "checkout", "stage", "status", "visitor", "source", "startedAt"]
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.primaryCurrency;
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 isRecord2(value) {
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 (!isRecord2(raw) || !Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) return null;
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 (!isRecord2(candidate)) continue;
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: isRecord2(config) ? 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 (!isRecord2(candidate)) continue;
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 (!isRecord2(raw)) return null;
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 = isRecord2(node.config.action) ? optionalString(node.config.action.type) : void 0;
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 = isRecord2(node.config.ownerNotification) ? node.config.ownerNotification : {};
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 = isRecord2(node.config.sms) ? node.config.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 = isRecord2(node.config.webhook) ? node.config.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 = isRecord2(node.config.social) ? node.config.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 = isRecord2(node.config.communityPost) ? node.config.communityPost : {};
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 = isRecord2(node.config.clientTags) ? node.config.clientTags : {};
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 = isRecord2(node.config.emailSubscription) ? node.config.emailSubscription : {};
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 = isRecord2(node.config.clientDeadline) ? node.config.clientDeadline : {};
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(isRecord2) : [];
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
- 'Workspace-level settings: the name, the scheduling timezone (workflow "wait until" steps and other workspace-scoped scheduling) and the video transcription defaults'
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.4.0".length > 0) {
29886
- return "0.4.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, clearCredentials);
30421
+ registerLogoutCommand(program2);
29928
30422
  registerAuthCommands(program2);
29929
30423
  registerProductsCommands(program2);
29930
30424
  registerOffersCommands(program2);