@zanfia/cli 0.3.63 → 0.6.4

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 +627 -170
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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,12 @@ 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
+ },
8449
+ async sendEmailCampaignTest() {
8450
+ throw new ApiError(501, "email campaigns are not available in mock mode");
8451
+ },
8346
8452
  async resendEmailCampaign() {
8347
8453
  throw new ApiError(501, "email campaigns are not available in mock mode");
8348
8454
  },
@@ -8511,6 +8617,9 @@ function createMockClient() {
8511
8617
  async removeDownloadFile() {
8512
8618
  throw new ApiError(501, "downloads content is not available in mock mode");
8513
8619
  },
8620
+ async renameDownloadFile() {
8621
+ throw new ApiError(501, "downloads content is not available in mock mode");
8622
+ },
8514
8623
  async reorderDownloadFiles() {
8515
8624
  throw new ApiError(501, "downloads content is not available in mock mode");
8516
8625
  },
@@ -9042,6 +9151,7 @@ function createHttpClient(config) {
9042
9151
  createProduct: (input) => request("POST", "/products", input),
9043
9152
  updateProduct: (id, input) => request("PATCH", `/products/${encodeURIComponent(id)}`, input),
9044
9153
  archiveProduct: (id) => request("POST", `/products/${encodeURIComponent(id)}/archive`),
9154
+ deleteProduct: (id) => request("DELETE", `/products/${encodeURIComponent(id)}`),
9045
9155
  setProductStatus: (id, status) => request("POST", `/products/${encodeURIComponent(id)}/status`, { status }),
9046
9156
  listProductPrices: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/prices`),
9047
9157
  createProductPrice: (productId, input) => request("POST", `/products/${encodeURIComponent(productId)}/prices`, input),
@@ -9120,6 +9230,8 @@ function createHttpClient(config) {
9120
9230
  if (options?.limit !== void 0) params.set("limit", String(options.limit));
9121
9231
  if (options?.cursor) params.set("cursor", options.cursor);
9122
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);
9123
9235
  const qs = params.toString();
9124
9236
  return request("GET", `/clients${qs ? `?${qs}` : ""}`);
9125
9237
  },
@@ -9535,6 +9647,12 @@ function createHttpClient(config) {
9535
9647
  scheduledAt !== void 0 ? { scheduledAt } : {}
9536
9648
  ),
9537
9649
  cancelEmailCampaign: (id) => request("POST", `/campaigns/${encodeURIComponent(id)}/cancel`),
9650
+ rescheduleEmailCampaign: (id, scheduledAt) => request("POST", `/campaigns/${encodeURIComponent(id)}/reschedule`, { scheduledAt }),
9651
+ sendEmailCampaignTest: (id, recipients) => request(
9652
+ "POST",
9653
+ `/campaigns/${encodeURIComponent(id)}/test-sends`,
9654
+ recipients && recipients.length > 0 ? { recipients } : {}
9655
+ ),
9538
9656
  resendEmailCampaign: (id, target) => request("POST", `/campaigns/${encodeURIComponent(id)}/resend`, target ? { target } : {}),
9539
9657
  listCampaignRecipients: (id, options) => {
9540
9658
  const params = new URLSearchParams();
@@ -9661,6 +9779,11 @@ function createHttpClient(config) {
9661
9779
  "DELETE",
9662
9780
  `/products/${encodeURIComponent(productId)}/downloads/files/${encodeURIComponent(uid)}`
9663
9781
  ),
9782
+ renameDownloadFile: (productId, uid, name) => request(
9783
+ "PATCH",
9784
+ `/products/${encodeURIComponent(productId)}/downloads/files/${encodeURIComponent(uid)}`,
9785
+ { name }
9786
+ ),
9664
9787
  reorderDownloadFiles: (productId, uids) => request("POST", `/products/${encodeURIComponent(productId)}/downloads/files/reorder`, {
9665
9788
  uids
9666
9789
  }),
@@ -10018,6 +10141,22 @@ function parseWorkflowKind(value) {
10018
10141
  }
10019
10142
  return kind;
10020
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
+ }
10021
10160
  function parseCommaList(flag) {
10022
10161
  return (value) => {
10023
10162
  const items = [
@@ -10057,6 +10196,8 @@ function requireConfirm(confirm, what) {
10057
10196
  var OUTPUT_FORMAT_JSON = "json";
10058
10197
  var API_KEY_FLAG = "--api-key <key>";
10059
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`)";
10060
10201
  var VERBOSE_FLAG = "--verbose";
10061
10202
  var VERBOSE_DESCRIPTION = "log request/response debug info";
10062
10203
  var JSON_FLAG = "--json";
@@ -10076,6 +10217,7 @@ function registerOnCommand(command) {
10076
10217
  command.enablePositionalOptions();
10077
10218
  }
10078
10219
  addOptionIfMissing(command, new Option(API_KEY_FLAG, API_KEY_DESCRIPTION).hideHelp());
10220
+ addOptionIfMissing(command, new Option(WORKSPACE_FLAG, WORKSPACE_DESCRIPTION).hideHelp());
10079
10221
  addOptionIfMissing(command, new Option(VERBOSE_FLAG, VERBOSE_DESCRIPTION).hideHelp());
10080
10222
  addOptionIfMissing(
10081
10223
  command,
@@ -10087,13 +10229,14 @@ function registerOnCommand(command) {
10087
10229
  function applyGlobalOptions(program2) {
10088
10230
  program2.enablePositionalOptions();
10089
10231
  program2.option(API_KEY_FLAG, API_KEY_DESCRIPTION);
10232
+ program2.option(WORKSPACE_FLAG, WORKSPACE_DESCRIPTION);
10090
10233
  program2.option(JSON_FLAG, JSON_DESCRIPTION);
10091
10234
  program2.option(VERBOSE_FLAG, VERBOSE_DESCRIPTION);
10092
10235
  for (const sub of program2.commands) registerOnCommand(sub);
10093
10236
  }
10094
10237
 
10095
10238
  // src/utils/output.ts
10096
- function isRecord(value) {
10239
+ function isRecord2(value) {
10097
10240
  return value !== null && typeof value === "object";
10098
10241
  }
10099
10242
  function getGlobalFlags(command) {
@@ -10104,11 +10247,15 @@ function getGlobalFlags(command) {
10104
10247
  const flags = {};
10105
10248
  for (const node of chain) {
10106
10249
  const raw = node.opts();
10107
- if (!isRecord(raw)) continue;
10250
+ if (!isRecord2(raw)) continue;
10108
10251
  if (typeof raw.apiKey === "string") flags.apiKey = raw.apiKey;
10252
+ if (typeof raw.workspace === "string") flags.workspace = raw.workspace;
10109
10253
  if (raw.verbose === true) flags.verbose = true;
10110
10254
  if (raw.json === true || raw.output === OUTPUT_FORMAT_JSON) flags.json = true;
10111
10255
  }
10256
+ if (flags.workspace && !flags.apiKey && !process.env.ZANFIA_API_KEY) {
10257
+ flags.apiKey = apiKeyForStoredWorkspace(flags.workspace);
10258
+ }
10112
10259
  return flags;
10113
10260
  }
10114
10261
  function emit(flags, data, humanRenderer) {
@@ -10124,10 +10271,10 @@ function printJson(data) {
10124
10271
  `);
10125
10272
  }
10126
10273
  function tableRowsOf(result, key) {
10127
- if (!isRecord(result)) return [];
10274
+ if (!isRecord2(result)) return [];
10128
10275
  const rows = result[key];
10129
10276
  if (!Array.isArray(rows)) return [];
10130
- return rows.filter(isRecord);
10277
+ return rows.filter(isRecord2);
10131
10278
  }
10132
10279
  function printTable(rows, columns) {
10133
10280
  if (rows.length === 0) {
@@ -11150,16 +11297,93 @@ More ads available \u2014 re-run with --after ${result.after}
11150
11297
  }
11151
11298
 
11152
11299
  // src/commands/auth/index.ts
11300
+ function keySource(flags, rawApiKey) {
11301
+ if (typeof rawApiKey === "string") return "--api-key";
11302
+ if (process.env.ZANFIA_API_KEY) return "$ZANFIA_API_KEY";
11303
+ if (flags.workspace) return `stored login of --workspace ${flags.workspace}`;
11304
+ return "active stored login";
11305
+ }
11306
+ async function runWhoami(command) {
11307
+ const flags = getGlobalFlags(command);
11308
+ const rawApiKey = command.optsWithGlobals().apiKey;
11309
+ const logins = listStoredProfiles().length;
11310
+ const apiKey = resolveApiKey(flags.apiKey);
11311
+ if (decodeTokenKind(apiKey) === "member") {
11312
+ const stored = readCredentials();
11313
+ const claims = decodeTokenClaims(apiKey);
11314
+ const workspaceId = stored?.workspaceId ?? claims.workspaceId ?? "?";
11315
+ const info2 = {
11316
+ role: "member",
11317
+ workspaceId,
11318
+ workspaceName: stored?.workspaceName ?? null,
11319
+ userEmail: stored?.userEmail ?? null,
11320
+ uid: claims.subject ?? null,
11321
+ scopes: ["communities:read", "communities:write"],
11322
+ communityApiUrl: resolveCommunityApiUrl()
11323
+ };
11324
+ emit(flags, info2, () => {
11325
+ process.stdout.write(`Role: community member
11326
+ `);
11327
+ process.stdout.write(
11328
+ `Workspace: ${info2.workspaceName ?? "(unknown)"} (${info2.workspaceId})
11329
+ `
11330
+ );
11331
+ if (info2.userEmail) process.stdout.write(`User: ${info2.userEmail}
11332
+ `);
11333
+ else if (info2.uid) process.stdout.write(`User id: ${info2.uid}
11334
+ `);
11335
+ process.stdout.write(`Scopes: ${info2.scopes.join(", ")}
11336
+ `);
11337
+ process.stdout.write(`Community API: ${info2.communityApiUrl ?? "<unresolved>"}
11338
+ `);
11339
+ });
11340
+ return;
11341
+ }
11342
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11343
+ const info = await client.whoami();
11344
+ const source = keySource(flags, rawApiKey);
11345
+ emit(flags, { ...info, keySource: source, storedLogins: logins }, () => {
11346
+ const modeTag = isMockMode() ? " (mock)" : "";
11347
+ process.stdout.write(`Workspace: ${info.workspaceName}${modeTag}
11348
+ `);
11349
+ process.stdout.write(`Workspace ID: ${info.workspaceId}
11350
+ `);
11351
+ process.stdout.write(`User: ${info.userEmail}
11352
+ `);
11353
+ process.stdout.write(`Scopes: ${info.scopes.join(", ")}
11354
+ `);
11355
+ process.stdout.write(`Key: ${source}
11356
+ `);
11357
+ if (logins > 1) {
11358
+ process.stdout.write(
11359
+ `Logins on this device: ${logins} (\`workspace list\`, switch with \`workspace use <id|slug>\`)
11360
+ `
11361
+ );
11362
+ }
11363
+ });
11364
+ }
11153
11365
  function registerAuthCommands(program2) {
11154
11366
  const auth = program2.command("auth").description("Manage CLI authentication");
11367
+ program2.command("whoami").description("Show the active workspace and identity (same as `auth whoami`)").action(async (_, command) => runWhoami(command));
11155
11368
  auth.command("status").description("Show what the CLI currently has stored (path, source, expiry, api URL).").action(() => {
11156
11369
  const stored = readCredentials();
11157
11370
  const apiKey = resolveApiKey(void 0);
11158
11371
  const apiUrl = resolveApiUrl();
11159
11372
  const envKey = process.env.ZANFIA_API_KEY ? "(from $ZANFIA_API_KEY)" : "";
11160
11373
  const envUrl = process.env.ZANFIA_API_URL ? "(from $ZANFIA_API_URL)" : "";
11161
- process.stdout.write(`Credentials file: ${credentialsPath}
11374
+ const profiles = listStoredProfiles();
11375
+ process.stdout.write(`Credentials file: ${credentialsFilePath()}
11376
+ `);
11377
+ if (profiles.length > 1) {
11378
+ process.stdout.write(`Stored logins: ${profiles.length} (active one below)
11162
11379
  `);
11380
+ for (const profile of profiles) {
11381
+ process.stdout.write(
11382
+ ` ${profile.active ? "*" : " "} ${profile.credentials.workspaceName ?? "?"} (${profile.key})
11383
+ `
11384
+ );
11385
+ }
11386
+ }
11163
11387
  if (!stored) {
11164
11388
  process.stdout.write(` status: no credentials file found
11165
11389
  `);
@@ -11236,54 +11460,7 @@ Resolved by next call:
11236
11460
  );
11237
11461
  }
11238
11462
  });
11239
- auth.command("whoami").description("Show the authenticated identity (creator workspace, or community member)").action(async (_, command) => {
11240
- const flags = getGlobalFlags(command);
11241
- const apiKey = resolveApiKey(flags.apiKey);
11242
- if (decodeTokenKind(apiKey) === "member") {
11243
- const stored = readCredentials();
11244
- const claims = decodeTokenClaims(apiKey);
11245
- const workspaceId = stored?.workspaceId ?? claims.workspaceId ?? "?";
11246
- const info2 = {
11247
- role: "member",
11248
- workspaceId,
11249
- workspaceName: stored?.workspaceName ?? null,
11250
- userEmail: stored?.userEmail ?? null,
11251
- uid: claims.subject ?? null,
11252
- scopes: ["communities:read", "communities:write"],
11253
- communityApiUrl: resolveCommunityApiUrl()
11254
- };
11255
- emit(flags, info2, () => {
11256
- process.stdout.write(`Role: community member
11257
- `);
11258
- process.stdout.write(
11259
- `Workspace: ${info2.workspaceName ?? "(unknown)"} (${info2.workspaceId})
11260
- `
11261
- );
11262
- if (info2.userEmail) process.stdout.write(`User: ${info2.userEmail}
11263
- `);
11264
- else if (info2.uid) process.stdout.write(`User id: ${info2.uid}
11265
- `);
11266
- process.stdout.write(`Scopes: ${info2.scopes.join(", ")}
11267
- `);
11268
- process.stdout.write(`Community API: ${info2.communityApiUrl ?? "<unresolved>"}
11269
- `);
11270
- });
11271
- return;
11272
- }
11273
- const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
11274
- const info = await client.whoami();
11275
- emit(flags, info, () => {
11276
- const modeTag = isMockMode() ? " (mock)" : "";
11277
- process.stdout.write(`Workspace: ${info.workspaceName}${modeTag}
11278
- `);
11279
- process.stdout.write(`Workspace ID: ${info.workspaceId}
11280
- `);
11281
- process.stdout.write(`User: ${info.userEmail}
11282
- `);
11283
- process.stdout.write(`Scopes: ${info.scopes.join(", ")}
11284
- `);
11285
- });
11286
- });
11463
+ auth.command("whoami").description("Show the authenticated identity (creator workspace, or community member)").action(async (_, command) => runWhoami(command));
11287
11464
  }
11288
11465
 
11289
11466
  // src/commands/auth/login.ts
@@ -11482,25 +11659,54 @@ async function postJson(url, body) {
11482
11659
  }
11483
11660
  return JSON.parse(text);
11484
11661
  }
11662
+ function buildCreatorConsentParams(input) {
11663
+ return {
11664
+ redirect_uri: input.redirectUri,
11665
+ state: input.state,
11666
+ code_challenge: input.challenge,
11667
+ code_challenge_method: "S256",
11668
+ hostname: input.hostname,
11669
+ client: "zanfia-cli",
11670
+ ...input.workspace?.trim() ? { workspace: input.workspace.trim() } : null
11671
+ };
11672
+ }
11673
+ function buildRegisterUrl(consentUrl, email) {
11674
+ const consent = new URL(consentUrl);
11675
+ const continuePath = `${consent.pathname}${consent.search}`;
11676
+ const register = new URL("/auth/register", consent.origin);
11677
+ register.searchParams.set("continue", continuePath);
11678
+ register.searchParams.set("next", continuePath);
11679
+ if (email) register.searchParams.set("email", email);
11680
+ return register.toString();
11681
+ }
11485
11682
  async function runLogin(options) {
11683
+ await runBrowserAuth(options, (consentUrl) => consentUrl);
11684
+ }
11685
+ async function runBrowserRegister(options) {
11686
+ await runBrowserAuth(options, (consentUrl) => buildRegisterUrl(consentUrl, options.email));
11687
+ }
11688
+ async function runBrowserAuth(options, entryUrl) {
11486
11689
  const urls = resolveUrls(options.apiUrl);
11487
11690
  const state = randomBytes(32).toString("base64url");
11488
11691
  const { verifier, challenge } = generatePkce();
11489
11692
  const host = safeHostname();
11490
11693
  const server = await startLoopbackServer();
11491
11694
  const redirectUri = `http://127.0.0.1:${server.port}${CALLBACK_PATH}`;
11492
- const consentUrl = buildConsentUrl(urls.loginUrl, {
11493
- redirect_uri: redirectUri,
11494
- state,
11495
- code_challenge: challenge,
11496
- code_challenge_method: "S256",
11497
- hostname: host,
11498
- client: "zanfia-cli"
11499
- });
11500
- process.stdout.write(`Opening ${consentUrl}
11695
+ const consentUrl = buildConsentUrl(
11696
+ urls.loginUrl,
11697
+ buildCreatorConsentParams({
11698
+ redirectUri,
11699
+ state,
11700
+ challenge,
11701
+ hostname: host,
11702
+ workspace: options.workspace
11703
+ })
11704
+ );
11705
+ const openUrl = entryUrl(consentUrl);
11706
+ process.stdout.write(`Opening ${openUrl}
11501
11707
  `);
11502
11708
  process.stdout.write("Waiting for browser callback...\n\n");
11503
- const opened = await openBrowser(consentUrl);
11709
+ const opened = await openBrowser(openUrl);
11504
11710
  if (!opened) {
11505
11711
  process.stdout.write("Could not open browser automatically \u2014 paste the URL above.\n\n");
11506
11712
  }
@@ -11524,14 +11730,24 @@ async function runLogin(options) {
11524
11730
  expiresAt: exchanged.expiresAt,
11525
11731
  userEmail: exchanged.userEmail,
11526
11732
  workspaceId: exchanged.workspaceId,
11527
- workspaceName: exchanged.workspaceName
11733
+ workspaceName: exchanged.workspaceName,
11734
+ workspaceSlug: exchanged.workspaceSlug ?? null
11528
11735
  });
11529
11736
  process.stdout.write(`Logged in as ${exchanged.userEmail}.
11530
11737
  `);
11531
- process.stdout.write(`Workspace: ${exchanged.workspaceName} (${exchanged.workspaceId})
11532
- `);
11738
+ process.stdout.write(
11739
+ `Workspace: ${exchanged.workspaceName} (${exchanged.workspaceId}) - now active
11740
+ `
11741
+ );
11533
11742
  process.stdout.write(`Credentials stored at ${stored}
11534
11743
  `);
11744
+ const logins = listStoredProfiles().length;
11745
+ if (logins > 1) {
11746
+ process.stdout.write(
11747
+ `Logins on this device: ${logins}. Switch with \`zanfia workspace use <id|slug>\` (see \`zanfia workspace list\`).
11748
+ `
11749
+ );
11750
+ }
11535
11751
  if (!urls.apiUrl) {
11536
11752
  process.stdout.write(
11537
11753
  "\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"
@@ -11644,6 +11860,9 @@ function registerLoginCommand(auth) {
11644
11860
  auth.command("login").description(
11645
11861
  "Open a browser to authorize the CLI via OAuth (pass --address to sign in as a community member)"
11646
11862
  ).option("--api-url <url>", "API URL to associate with the resulting token").option(
11863
+ "--workspace <id|slug>",
11864
+ "preselect this workspace on the consent page (you can still pick another there)"
11865
+ ).option(
11647
11866
  "--address <url>",
11648
11867
  "sign in as a community member (Community API) against this creator platform URL"
11649
11868
  ).option("-v, --verbose", "show HTTP request details").addHelpText(
@@ -11652,9 +11871,13 @@ function registerLoginCommand(auth) {
11652
11871
  The two flows use different consent pages:
11653
11872
 
11654
11873
  Creator (workspace) login \u2014 full Workspace API:
11655
- login
11874
+ login [--workspace <id|slug>]
11656
11875
  Opens the consent page in the Zanfia dashboard. Sign in with your
11657
- creator account and approve; the CLI gets a workspace token.
11876
+ creator account, pick the workspace (Owner or Admin) and approve; the CLI
11877
+ gets a token for that workspace. Each workspace keeps its own login on
11878
+ this device: \`workspace list\` shows them, \`workspace use <id|slug>\`
11879
+ switches without logging in again, and \`--workspace <id|slug>\` on any
11880
+ command uses another stored login just for that command.
11658
11881
 
11659
11882
  Community member login \u2014 Community API only:
11660
11883
  login --address https://<creator-platform-url>
@@ -11678,14 +11901,27 @@ The two flows use different consent pages:
11678
11901
  await runLogin(options);
11679
11902
  });
11680
11903
  }
11681
- function registerLogoutCommand(auth, clear) {
11682
- auth.command("logout").description("Forget the stored CLI credentials").action(() => {
11683
- const cleared = clear();
11684
- if (cleared) {
11685
- process.stdout.write(`Removed credentials at ${credentialsPath}.
11686
- `);
11687
- } else {
11904
+ function registerLogoutCommand(auth) {
11905
+ auth.command("logout").description(
11906
+ "Forget the active stored login (--all forgets every workspace login on this device)"
11907
+ ).option("--all", "remove every stored login").action((options) => {
11908
+ const { removed, nowActive } = removeStoredLogin({ all: options.all });
11909
+ if (!removed && !options.all) {
11688
11910
  process.stdout.write("No stored credentials to remove.\n");
11911
+ return;
11912
+ }
11913
+ if (options.all) {
11914
+ process.stdout.write("Removed every stored login.\n");
11915
+ return;
11916
+ }
11917
+ const label = removed?.workspaceName ?? removed?.workspaceId ?? "the stored login";
11918
+ process.stdout.write(`Logged out of ${label}.
11919
+ `);
11920
+ if (nowActive) {
11921
+ process.stdout.write(
11922
+ `Active now: ${nowActive.workspaceName ?? "?"} (${nowActive.workspaceId ?? "?"})
11923
+ `
11924
+ );
11689
11925
  }
11690
11926
  });
11691
11927
  }
@@ -11695,6 +11931,16 @@ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
11695
11931
 
11696
11932
  // src/utils/httpJson.ts
11697
11933
  import { hostname as hostname2 } from "os";
11934
+ var HttpJsonError = class extends Error {
11935
+ status;
11936
+ code;
11937
+ constructor(message, status, code) {
11938
+ super(message);
11939
+ this.name = "HttpJsonError";
11940
+ this.status = status;
11941
+ this.code = code;
11942
+ }
11943
+ };
11698
11944
  async function postJson2(label, url, body, options = {}) {
11699
11945
  if (options.verbose) process.stderr.write(`\u2192 POST ${url}
11700
11946
  `);
@@ -11710,8 +11956,10 @@ async function postJson2(label, url, body, options = {}) {
11710
11956
  });
11711
11957
  const text = await response.text();
11712
11958
  if (!response.ok) {
11713
- throw new Error(
11714
- `${label} failed (${response.status}): ${extractErrorMessage(text, response.statusText)}`
11959
+ throw new HttpJsonError(
11960
+ `${label} failed (${response.status}): ${extractErrorMessage(text, response.statusText)}`,
11961
+ response.status,
11962
+ extractErrorCode(text)
11715
11963
  );
11716
11964
  }
11717
11965
  return JSON.parse(text);
@@ -11726,6 +11974,14 @@ function extractErrorMessage(text, fallback) {
11726
11974
  }
11727
11975
  return text || fallback;
11728
11976
  }
11977
+ function extractErrorCode(text) {
11978
+ try {
11979
+ const parsed = JSON.parse(text);
11980
+ return typeof parsed.code === "string" ? parsed.code : null;
11981
+ } catch {
11982
+ return null;
11983
+ }
11984
+ }
11729
11985
  function safeHostname2() {
11730
11986
  try {
11731
11987
  const h = hostname2();
@@ -11782,6 +12038,7 @@ function promptHidden(question) {
11782
12038
  }
11783
12039
 
11784
12040
  // src/commands/auth/register.ts
12041
+ var BOT_CHECK_FAILED_CODE = "auth/bot-check-failed";
11785
12042
  var MAX_CODE_ATTEMPTS = 5;
11786
12043
  var MIN_PASSWORD_LENGTH = 8;
11787
12044
  var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -11825,6 +12082,32 @@ async function runRegister(options) {
11825
12082
  if (!EMAIL_REGEX.test(email)) {
11826
12083
  throw new Error(`"${email}" does not look like a valid email address.`);
11827
12084
  }
12085
+ const initiateBody = {
12086
+ email,
12087
+ lang,
12088
+ accessCode: options.accessCode ?? null
12089
+ };
12090
+ try {
12091
+ await postJson2(
12092
+ "Sending verification code",
12093
+ `${urls.usersUrl}/initiate-registration`,
12094
+ initiateBody,
12095
+ { verbose }
12096
+ );
12097
+ } catch (error) {
12098
+ if (error instanceof HttpJsonError && error.code === BOT_CHECK_FAILED_CODE) {
12099
+ process.stdout.write(
12100
+ "\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"
12101
+ );
12102
+ await runBrowserRegister({ email, apiUrl: options.apiUrl, verbose });
12103
+ return;
12104
+ }
12105
+ throw error;
12106
+ }
12107
+ process.stdout.write(`
12108
+ We sent a 6-digit code to ${email}. Check your inbox.
12109
+
12110
+ `);
11828
12111
  const fullName = options.name ?? await promptLine("Full name: ");
11829
12112
  if (fullName.length < 3 || fullName.length > 100) {
11830
12113
  throw new Error("Full name must be 3-100 characters.");
@@ -11851,21 +12134,6 @@ async function runRegister(options) {
11851
12134
  } else if (password.length < MIN_PASSWORD_LENGTH) {
11852
12135
  throw new Error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters.`);
11853
12136
  }
11854
- const initiateBody = {
11855
- email,
11856
- lang,
11857
- accessCode: options.accessCode ?? null
11858
- };
11859
- await postJson2(
11860
- "Sending verification code",
11861
- `${urls.usersUrl}/initiate-registration`,
11862
- initiateBody,
11863
- { verbose }
11864
- );
11865
- process.stdout.write(`
11866
- We sent a 6-digit code to ${email}. Check your inbox.
11867
-
11868
- `);
11869
12137
  let verificationToken = null;
11870
12138
  for (let attempt = 1; attempt <= MAX_CODE_ATTEMPTS; attempt++) {
11871
12139
  const code = options.code ?? await promptLine("Verification code (or 'r' to resend): ");
@@ -11970,16 +12238,18 @@ Logged in as ${exchanged.userEmail}.
11970
12238
  }
11971
12239
  }
11972
12240
  function registerRegisterCommand(program2) {
11973
- 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(
12241
+ 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(
11974
12242
  "--password <password>",
11975
12243
  "optional account password (min 8 chars); prefer the interactive prompt \u2014 flags leak into shell history"
11976
12244
  ).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(
11977
12245
  "after",
11978
12246
  `
11979
- The same email-code registration as the Zanfia dashboard, entirely in the
11980
- terminal: you get a 6-digit code by email, the account plus its personal
11981
- workspace are created, and the CLI ends up logged in exactly as if you had
11982
- run \`login\` \u2014 no browser needed.
12247
+ New accounts finish signing up in the browser: Zanfia runs a quick security
12248
+ check on the signup page, which a terminal cannot do. The CLI opens the
12249
+ signup page with your email filled in, and once the account exists it logs
12250
+ in exactly as if you had run \`login\`. Where the check does not apply, the
12251
+ whole flow stays in the terminal: a 6-digit code by email, then the account
12252
+ plus its personal workspace are created.
11983
12253
 
11984
12254
  Running it with an email that already has a creator account simply signs
11985
12255
  that account in.
@@ -12299,6 +12569,9 @@ var RECIPIENT_STATUSES = [
12299
12569
  "complained",
12300
12570
  "clicked"
12301
12571
  ];
12572
+ function collectRepeatable(value, previous) {
12573
+ return [...previous, value];
12574
+ }
12302
12575
  function parsePositiveInt2(value, flag) {
12303
12576
  const n = Number.parseInt(value, 10);
12304
12577
  if (!Number.isInteger(n) || n <= 0) {
@@ -12534,6 +12807,25 @@ function registerCampaignsCommands(program2) {
12534
12807
  `);
12535
12808
  });
12536
12809
  });
12810
+ campaigns.command("test").description(
12811
+ "Send a TEST of the saved campaign to up to 3 addresses (default: your own) \u2014 subject prefixed [Test], sample personalization values; never the audience"
12812
+ ).argument("<id>", "campaign id").option(
12813
+ "--to <email>",
12814
+ "test recipient \u2014 repeatable or comma-separated, max 3 (default: your own address)",
12815
+ collectRepeatable,
12816
+ []
12817
+ ).action(async (id, options, command) => {
12818
+ const flags = getGlobalFlags(command);
12819
+ const recipients = options.to.flatMap((entry) => entry.split(",")).map((email) => email.trim()).filter(Boolean);
12820
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12821
+ const result = await client.sendEmailCampaignTest(id, recipients);
12822
+ emit(flags, result, () => {
12823
+ process.stdout.write(
12824
+ `Test of campaign ${id} queued \u2192 ${result.recipients.join(", ")} (subject prefixed [Test]). Nothing was sent to the audience.
12825
+ `
12826
+ );
12827
+ });
12828
+ });
12537
12829
  campaigns.command("send").description(
12538
12830
  "\u26A0\uFE0F SENDS the campaign \u2014 REAL bulk email to the entire resolved audience, IRREVERSIBLY. Delivered mail cannot be recalled; `campaigns cancel` only stops remaining sends. Requires --confirm. Omit --at to send NOW; --at schedules (max 30 days out)."
12539
12831
  ).argument("<id>", "campaign id").option(
@@ -12543,7 +12835,7 @@ function registerCampaignsCommands(program2) {
12543
12835
  const flags = getGlobalFlags(command);
12544
12836
  if (!options.confirm) {
12545
12837
  throw new Error(
12546
- `Refusing to send without --confirm. \`campaigns send\` emails the ENTIRE resolved audience for real and cannot be undone once mail is delivered. Re-run with --confirm to proceed (check the draft first with \`campaigns get ${id}\`).`
12838
+ `Refusing to send without --confirm. \`campaigns send\` emails the ENTIRE resolved audience for real and cannot be undone once mail is delivered. Re-run with --confirm to proceed (check the draft first with \`campaigns get ${id}\` and \`campaigns test ${id}\`).`
12547
12839
  );
12548
12840
  }
12549
12841
  if (options.at !== void 0 && Number.isNaN(Date.parse(options.at))) {
@@ -12578,6 +12870,28 @@ function registerCampaignsCommands(program2) {
12578
12870
  const { stats } = result.campaign;
12579
12871
  process.stdout.write(
12580
12872
  `Cancelled campaign ${result.campaign.id} \u2014 ${stats.sent} recipient${stats.sent === 1 ? " was" : "s were"} already sent to before the cancel.
12873
+ `
12874
+ );
12875
+ });
12876
+ });
12877
+ campaigns.command("reschedule").description(
12878
+ "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."
12879
+ ).argument("<id>", "campaign id (must be scheduled)").requiredOption(
12880
+ "--at <isoTimestamp>",
12881
+ "new send time (ISO with offset, e.g. 2026-10-15T09:00:00+02:00)"
12882
+ ).action(async (id, options, command) => {
12883
+ const flags = getGlobalFlags(command);
12884
+ if (Number.isNaN(Date.parse(options.at))) {
12885
+ throw new Error(
12886
+ `Invalid --at timestamp: ${options.at} (expected ISO, e.g. 2026-10-15T09:00:00+02:00)`
12887
+ );
12888
+ }
12889
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12890
+ const result = await client.rescheduleEmailCampaign(id, options.at);
12891
+ emit(flags, result, () => {
12892
+ const { campaign } = result;
12893
+ process.stdout.write(
12894
+ `Campaign ${campaign.id} moved to ${campaign.scheduledAt} \u2014 still scheduled; stop it before then with \`campaigns cancel\`.
12581
12895
  `
12582
12896
  );
12583
12897
  });
@@ -13567,7 +13881,7 @@ function parsePositiveInt4(value, flag) {
13567
13881
  }
13568
13882
  return n;
13569
13883
  }
13570
- function collectRepeatable(value, previous) {
13884
+ function collectRepeatable2(value, previous) {
13571
13885
  return [...previous, value];
13572
13886
  }
13573
13887
  function parseCsv(raw) {
@@ -13726,7 +14040,15 @@ function printMergePreview(preview) {
13726
14040
  }
13727
14041
  function registerClientsCommands(program2) {
13728
14042
  const clients2 = program2.command("clients").description("Manage workspace clients (customers)");
13729
- 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(
14043
+ clients2.command("list").description(
14044
+ "List clients (newest first), with optional search, product and date-added filters"
14045
+ ).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(
14046
+ "--created-from <date>",
14047
+ "only clients added on or after this date (YYYY-MM-DD or ISO 8601 date-time)"
14048
+ ).option(
14049
+ "--created-to <date>",
14050
+ "only clients added on or before this date (YYYY-MM-DD keeps the whole day)"
14051
+ ).action(
13730
14052
  async (options, command) => {
13731
14053
  const flags = getGlobalFlags(command);
13732
14054
  if (options.withProducts && options.withoutProducts) {
@@ -13737,7 +14059,9 @@ function registerClientsCommands(program2) {
13737
14059
  search: options.search,
13738
14060
  limit: options.limit !== void 0 ? parsePositiveInt4(options.limit, "--limit") : void 0,
13739
14061
  cursor: options.cursor,
13740
- products: options.withProducts ? "has" : options.withoutProducts ? "no" : void 0
14062
+ products: options.withProducts ? "has" : options.withoutProducts ? "no" : void 0,
14063
+ createdFrom: options.createdFrom,
14064
+ createdTo: options.createdTo
13741
14065
  });
13742
14066
  emit(flags, page, () => {
13743
14067
  printTable(
@@ -14051,7 +14375,7 @@ Next page: --cursor ${page.nextCursor}
14051
14375
  ).argument("<csvFile>", "path to the CSV file").option(
14052
14376
  "--tag <tagId>",
14053
14377
  "existing tag id applied to every imported row (repeat for several; unknown ids fail)",
14054
- collectRepeatable,
14378
+ collectRepeatable2,
14055
14379
  []
14056
14380
  ).action(async (csvFile, options, command) => {
14057
14381
  const flags = getGlobalFlags(command);
@@ -15969,7 +16293,7 @@ function assertStatus(status) {
15969
16293
  throw new Error(`Invalid status "${status}". Use one of: ${LESSON_STATUSES.join(", ")}`);
15970
16294
  }
15971
16295
  }
15972
- function collectRepeatable2(value, previous) {
16296
+ function collectRepeatable3(value, previous) {
15973
16297
  return [...previous, value];
15974
16298
  }
15975
16299
  function parseNonNegativeInt(value, flag) {
@@ -16577,7 +16901,7 @@ ${NOT_SAVED_NOTE} --moments=${options.out ?? "<file>"} --confirm\`.
16577
16901
  ).argument("<productId>", "course product id").argument("<lessonId>", "quiz lesson id").option("--type <type>", "single | multi | truefalse").option("--question <text>", "the question prompt").option("--explanation <text>", "explanation shown on the results screen").option("--points <n>", "points awarded (default 1)").option(
16578
16902
  "--option <text>",
16579
16903
  "an answer option (repeat for several; choice types only)",
16580
- collectRepeatable2,
16904
+ collectRepeatable3,
16581
16905
  []
16582
16906
  ).option("--correct <indices>", "1-based, comma-separated indices of the correct option(s)").option("--correct-answer <bool>", "true/false questions: the correct answer").option("--json <fileOrJson>", "full QuizQuestionInput object as inline JSON or a .json path").action(
16583
16907
  async (productId, lessonId, options, command) => {
@@ -16593,7 +16917,7 @@ ${NOT_SAVED_NOTE} --moments=${options.out ?? "<file>"} --confirm\`.
16593
16917
  quiz.command("update").description("Replace a question in full (same flags as `quiz add`, or --json)").argument("<productId>", "course product id").argument("<lessonId>", "quiz lesson id").argument("<questionId>", "question id").option("--type <type>", "single | multi | truefalse").option("--question <text>", "the question prompt").option("--explanation <text>", "explanation shown on the results screen").option("--points <n>", "points awarded (default 1)").option(
16594
16918
  "--option <text>",
16595
16919
  "an answer option (repeat for several; choice types only)",
16596
- collectRepeatable2,
16920
+ collectRepeatable3,
16597
16921
  []
16598
16922
  ).option("--correct <indices>", "1-based, comma-separated indices of the correct option(s)").option("--correct-answer <bool>", "true/false questions: the correct answer").option("--json <fileOrJson>", "full QuizQuestionInput object as inline JSON or a .json path").action(
16599
16923
  async (productId, lessonId, questionId, options, command) => {
@@ -16777,7 +17101,7 @@ function parseIsoDate(value, flag) {
16777
17101
  }
16778
17102
  return value;
16779
17103
  }
16780
- function collectRepeatable3(value, previous) {
17104
+ function collectRepeatable4(value, previous) {
16781
17105
  return [...previous, value];
16782
17106
  }
16783
17107
  function registerDiscountsCommands(program2) {
@@ -16802,7 +17126,7 @@ function registerDiscountsCommands(program2) {
16802
17126
  discounts2.command("create").description("Create a discount code (exactly one of --percent-off / --amount-off)").requiredOption("--code <code>", "discount code (e.g. LAUNCH20)").option("--percent-off <pct>", "percentage discount (> 0, <= 100)").option("--amount-off <minorUnits>", "fixed discount in minor units (e.g. 1000 = 10.00)").option("--currency <currency>", "ISO currency code (required with --amount-off)").option(
16803
17127
  "--product <id>",
16804
17128
  "limit to a product (repeat the flag for several)",
16805
- collectRepeatable3,
17129
+ collectRepeatable4,
16806
17130
  []
16807
17131
  ).option("--max-redemptions <n>", "maximum number of redemptions").option("--ends <isoDate>", "date the code stops working (ISO, e.g. 2026-12-31)").option("--starts <isoDate>", "date the code starts working (ISO, e.g. 2026-12-01)").option("--forever", "apply to every renewal of a subscription (default: first payment only)").option("--inactive", "create the code deactivated (default: active)").action(
16808
17132
  async (options, command) => {
@@ -16852,7 +17176,7 @@ function registerDiscountsCommands(program2) {
16852
17176
  discounts2.command("update").description("Update a discount code (value, products, limits, dates, active state)").argument("<discountId>", "discount id").option("--percent-off <pct>", "switch to a percentage discount (> 0, <= 100)").option("--amount-off <minorUnits>", "switch to a fixed discount in minor units").option("--currency <currency>", "ISO currency code for --amount-off").option(
16853
17177
  "--product <id>",
16854
17178
  "replace the product restriction (repeat the flag for several)",
16855
- collectRepeatable3,
17179
+ collectRepeatable4,
16856
17180
  []
16857
17181
  ).option("--max-redemptions <n>", "maximum number of redemptions").option("--clear-max-redemptions", "remove the redemption limit").option("--ends <isoDate>", "date the code stops working (ISO, e.g. 2026-12-31)").option("--clear-ends", "remove the end date").option("--forever", "apply to every renewal of a subscription").option("--active", "activate the code").option("--inactive", "deactivate the code").action(
16858
17182
  async (discountId, options, command) => {
@@ -19267,9 +19591,14 @@ join again to refresh it.
19267
19591
  // src/commands/liveRooms/index.ts
19268
19592
  var minutes = (seconds) => `${Math.round(seconds / 60)} min`;
19269
19593
  var percent = (part, whole) => whole > 0 ? `${Math.round(part / whole * 100)}%` : "-";
19594
+ var WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
19270
19595
  function describeSchedule(room) {
19271
19596
  if (!room.schedule) return "fixed";
19272
19597
  const until = room.schedule.until ? ` until ${room.schedule.until}` : "";
19598
+ if (room.schedule.kind === "daily") {
19599
+ const days = room.schedule.weekdays ? room.schedule.weekdays.map((day) => WEEKDAYS[day - 1] ?? String(day)).join(", ") : "daily";
19600
+ return `${days} at ${room.schedule.times.join(", ")} (${room.timezone})${until}`;
19601
+ }
19273
19602
  return `every ${room.schedule.everyMinutes} min${until}`;
19274
19603
  }
19275
19604
  function registerLiveRoomsCommands(program2) {
@@ -23043,7 +23372,7 @@ function parseJsonFileOrInline5(value, flag) {
23043
23372
  throw new Error(`Invalid JSON for ${flag}: ${error.message}`);
23044
23373
  }
23045
23374
  }
23046
- function collectRepeatable4(value, previous) {
23375
+ function collectRepeatable5(value, previous) {
23047
23376
  return [...previous, value];
23048
23377
  }
23049
23378
  function registerNewslettersCommands(program2) {
@@ -23374,7 +23703,7 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
23374
23703
  ).option("--double-opt-in <mode>", `subscription confirmation (${DOUBLE_OPT_INS.join("|")})`).option(
23375
23704
  "--client-tag <tag>",
23376
23705
  "tag applied to new subscribers (repeat the flag; replaces the list)",
23377
- collectRepeatable4,
23706
+ collectRepeatable5,
23378
23707
  []
23379
23708
  ).option(
23380
23709
  "--delivery <mode>",
@@ -25064,6 +25393,22 @@ import { readFile as readFile3 } from "fs/promises";
25064
25393
  import { basename as basename2 } from "path";
25065
25394
  var KB_TREE_CHANGED_ERROR = "tree-changed";
25066
25395
  var JSON_FLAG_HINT3 = "(write it as --json=<value> \u2014 a bare --json is the global JSON-output flag)";
25396
+ var PRODUCT_TYPE_INPUTS = [
25397
+ "downloads",
25398
+ "download",
25399
+ "newsletter",
25400
+ "course",
25401
+ "mailing",
25402
+ "community",
25403
+ "membership",
25404
+ "knowledge_base",
25405
+ "one_time",
25406
+ "custom",
25407
+ "event",
25408
+ "consultation",
25409
+ "service",
25410
+ "bundle"
25411
+ ];
25067
25412
  function parseCoverRef(value) {
25068
25413
  const separator = value.indexOf(":");
25069
25414
  if (separator < 0) {
@@ -25174,7 +25519,7 @@ function parseJsonFileOrInline8(value, flag) {
25174
25519
  throw new Error(`Invalid JSON for ${flag}: ${error.message}`);
25175
25520
  }
25176
25521
  }
25177
- function collectRepeatable5(value, previous) {
25522
+ function collectRepeatable6(value, previous) {
25178
25523
  return [...previous, value];
25179
25524
  }
25180
25525
  var HTTP_URL_PATTERN2 = /^https?:\/\//i;
@@ -25252,6 +25597,10 @@ function registerProductsCommands(program2) {
25252
25597
  `);
25253
25598
  process.stdout.write(` Status: ${product.status}
25254
25599
  `);
25600
+ if (product.storefrontVisibility) {
25601
+ process.stdout.write(` Visibility: ${product.storefrontVisibility}
25602
+ `);
25603
+ }
25255
25604
  process.stdout.write(` Clients: ${product.clientCount}
25256
25605
  `);
25257
25606
  if (product.description) {
@@ -25262,7 +25611,10 @@ function registerProductsCommands(program2) {
25262
25611
  });
25263
25612
  products2.command("create").description(
25264
25613
  "Create a new product. Community/newsletter types seed their full content (community: channels doc, built-in roles, owner member). Add --free or --amount to also seed a default offer \u2014 e.g. a paid community: --type community --amount 49.99 --currency usd --period monthly"
25265
- ).requiredOption("--name <name>", "product name").requiredOption("--type <type>", "product type (course, newsletter, community, ...)").option("--description <description>", "product description").option("--free", "seed a free default offer").option("--amount <amount>", "seed a paid default offer \u2014 price in MAJOR units (49.99)").option("--currency <currency>", "currency for --amount (pln, usd, eur, gbp)").option(
25614
+ ).requiredOption("--name <name>", "product name").requiredOption(
25615
+ "--type <type>",
25616
+ `product type: ${PRODUCT_TYPE_INPUTS.join(", ")} (downloads/download are stored as one_time, newsletter as mailing)`
25617
+ ).option("--description <description>", "product description").option("--free", "seed a free default offer").option("--amount <amount>", "seed a paid default offer \u2014 price in MAJOR units (49.99)").option("--currency <currency>", "currency for --amount (pln, usd, eur, gbp)").option(
25266
25618
  "--period <period>",
25267
25619
  "billing period for --amount: one_time, monthly or yearly (default one_time)"
25268
25620
  ).option("--description-file <path>", "read the product description from a file").option(
@@ -25271,7 +25623,7 @@ function registerProductsCommands(program2) {
25271
25623
  ).option(
25272
25624
  "--file <path>",
25273
25625
  "local file to upload and attach to the product (downloads products; repeat for several)",
25274
- collectRepeatable5,
25626
+ collectRepeatable6,
25275
25627
  []
25276
25628
  ).action(
25277
25629
  async (options, command) => {
@@ -25335,13 +25687,21 @@ function registerProductsCommands(program2) {
25335
25687
  });
25336
25688
  }
25337
25689
  );
25338
- 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(
25690
+ 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(
25691
+ "--visibility <public|unlisted|private>",
25692
+ "where buyers find it once published: public (store + product page), unlisted (product page by link only), private (checkout links only)",
25693
+ parseStorefrontVisibility
25694
+ ).action(
25339
25695
  async (id, options, command) => {
25696
+ if (options.name === void 0 && options.description === void 0 && options.visibility === void 0) {
25697
+ throw new Error("Nothing to update: pass --name, --description and/or --visibility.");
25698
+ }
25340
25699
  const flags = getGlobalFlags(command);
25341
25700
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25342
25701
  const result = await client.updateProduct(id, {
25343
25702
  name: options.name,
25344
- description: options.description
25703
+ description: options.description,
25704
+ storefrontVisibility: options.visibility
25345
25705
  });
25346
25706
  emit(flags, result, () => {
25347
25707
  process.stdout.write(`Updated product ${result.productId}
@@ -25358,6 +25718,26 @@ function registerProductsCommands(program2) {
25358
25718
  `);
25359
25719
  });
25360
25720
  });
25721
+ products2.command("delete").description(
25722
+ "Delete a product (soft-delete, like the dashboard): offers archived, customers lose access, subscriptions cancelled without refunds, data purged after 30 days. Requires --confirm."
25723
+ ).argument("<id>", "product id").option("--confirm", "required \u2014 acknowledge that customers lose access and subscriptions end").action(async (id, options, command) => {
25724
+ requireConfirm(options.confirm, `delete product ${id}`);
25725
+ const flags = getGlobalFlags(command);
25726
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25727
+ const result = await client.deleteProduct(id);
25728
+ emit(flags, result, () => {
25729
+ process.stdout.write(
25730
+ `Deleted product ${result.productId}; data is purged after ${new Date(result.purgeAfter * 1e3).toISOString().slice(0, 10)}.
25731
+ `
25732
+ );
25733
+ if (result.activeGrants > 0) {
25734
+ process.stdout.write(
25735
+ `${result.activeGrants} customer(s) lost access; ${result.cancellableSubscriptions} subscription(s) are being cancelled (run ${result.runId ?? "n/a"}).
25736
+ `
25737
+ );
25738
+ }
25739
+ });
25740
+ });
25361
25741
  products2.command("publish").description("Publish a product (status \u2192 published; live for customers)").argument("<id>", "product id").action(async (id, _, command) => {
25362
25742
  const flags = getGlobalFlags(command);
25363
25743
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -25606,7 +25986,7 @@ function registerProductsCommands(program2) {
25606
25986
  ).argument("<productId>", "appointment product id").option("--config <fileOrJson>", "full config object as inline JSON or a path to a .json file").option("--booking-method <method>", `consultation: ${BOOKING_METHODS.join("|")}`).option("--booking-url <url>", "consultation: external scheduler URL").option("--duration <minutes>", "consultation: session length in minutes").option("--location <text>", "consultation location / event join URL or address").option("--location-details <text>", "consultation: extra location details").option("--confirmation-message <text>", "consultation: post-booking confirmation message").option(
25607
25987
  "--deliverable <text>",
25608
25988
  "service: a deliverable line (repeat the flag; replaces the list)",
25609
- collectRepeatable5,
25989
+ collectRepeatable6,
25610
25990
  []
25611
25991
  ).option("--delivery-time <n>", "service: delivery time value").option("--delivery-unit <unit>", `service: ${DELIVERY_UNITS.join("|")} (with --delivery-time)`).option("--revision-rounds <n|unlimited>", 'service: revision rounds count or "unlimited"').option("--brief-method <method>", `service: ${BRIEF_METHODS.join("|")}`).option("--brief-url <url>", "service: external brief/intake form URL").option("--event-mode <mode>", `event: ${EVENT_MODES3.join("|")}`).option("--starts <iso>", "event: start (ISO 8601)").option("--ends <iso>", "event: end (ISO 8601)").option("--capacity <n>", "event: attendee capacity").option("--show-on-success <on|off>", "show the post-purchase CTA on the success page").action(
25612
25992
  async (productId, options, command) => {
@@ -25802,7 +26182,7 @@ function registerProductsCommands(program2) {
25802
26182
  ).argument("<productId>", "product id").requiredOption(
25803
26183
  "--image <url>",
25804
26184
  "an image URL (repeat the flag for several; the set fully replaces the current images)",
25805
- collectRepeatable5,
26185
+ collectRepeatable6,
25806
26186
  []
25807
26187
  ).action(async (productId, options, command) => {
25808
26188
  const flags = getGlobalFlags(command);
@@ -25902,6 +26282,18 @@ function registerProductsCommands(program2) {
25902
26282
  );
25903
26283
  });
25904
26284
  });
26285
+ files.command("rename").description(
26286
+ "Rename a file in place (its name is the buyer\u2019s download filename; order and uid stay)"
26287
+ ).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) => {
26288
+ const flags = getGlobalFlags(command);
26289
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
26290
+ const result = await client.renameDownloadFile(options.product, uid, options.name);
26291
+ emit(flags, result, () => {
26292
+ process.stdout.write(`Renamed ${result.file.uid} to "${result.file.name}".
26293
+ `);
26294
+ printFilesTable(result.files);
26295
+ });
26296
+ });
25905
26297
  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) => {
25906
26298
  const flags = getGlobalFlags(command);
25907
26299
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -26531,7 +26923,7 @@ Failed deliveries (${result.failedDeliveries.length})
26531
26923
  // src/commands/segments/index.ts
26532
26924
  import { readFileSync as readFileSync24 } from "fs";
26533
26925
  var SUBSCRIPTION_STATUSES2 = ["pending", "subscribed", "unsubscribed"];
26534
- function collectRepeatable6(value, previous) {
26926
+ function collectRepeatable7(value, previous) {
26535
26927
  return [...previous, value];
26536
26928
  }
26537
26929
  function parseMinorUnits(value, flag) {
@@ -26623,42 +27015,42 @@ function withConditionOptions(command) {
26623
27015
  return command.option(
26624
27016
  "--has-tag <tagId>",
26625
27017
  "client carries this tag (repeat for several \u2014 conditions are ANDed)",
26626
- collectRepeatable6,
27018
+ collectRepeatable7,
26627
27019
  []
26628
27020
  ).option(
26629
27021
  "--without-tag <tagId>",
26630
27022
  "client does NOT carry this tag (repeatable)",
26631
- collectRepeatable6,
27023
+ collectRepeatable7,
26632
27024
  []
26633
27025
  ).option(
26634
27026
  "--has-any-tag <tagId>",
26635
27027
  "client carries AT LEAST ONE of these tags (repeat the flag; one OR condition, max 10 ids) - one send to several tags without double-mailing",
26636
- collectRepeatable6,
27028
+ collectRepeatable7,
26637
27029
  []
26638
27030
  ).option(
26639
27031
  "--has-none-tag <tagId>",
26640
27032
  "client carries NONE of these tags (repeat the flag; one condition, max 10 ids)",
26641
- collectRepeatable6,
27033
+ collectRepeatable7,
26642
27034
  []
26643
27035
  ).option(
26644
27036
  "--owns <productId>",
26645
27037
  "client has ACTIVE access to this product - a direct grant OR through a bundle (repeatable)",
26646
- collectRepeatable6,
27038
+ collectRepeatable7,
26647
27039
  []
26648
27040
  ).option(
26649
27041
  "--not-owns <productId>",
26650
27042
  "client has NO active access to this product, direct or through a bundle (repeatable)",
26651
- collectRepeatable6,
27043
+ collectRepeatable7,
26652
27044
  []
26653
27045
  ).option(
26654
27046
  "--owns-direct <productId>",
26655
27047
  "client has an ACTIVE grant on this product itself (bought / granted by hand) - bundle access does not count; use to mail only the buyers of that one product (repeatable)",
26656
- collectRepeatable6,
27048
+ collectRepeatable7,
26657
27049
  []
26658
27050
  ).option(
26659
27051
  "--not-owns-direct <productId>",
26660
27052
  "client has NO active direct grant on this product - bundle holders DO match (repeatable)",
26661
- collectRepeatable6,
27053
+ collectRepeatable7,
26662
27054
  []
26663
27055
  ).option(
26664
27056
  "--spent-gte <minorUnits>",
@@ -26700,6 +27092,8 @@ function describeCondition(condition) {
26700
27092
  return condition.operator === "has" ? "has a phone number" : "has no phone number";
26701
27093
  case "campaign":
26702
27094
  return `${condition.operator.replace("-", " ")} campaign ${condition.campaignId}`;
27095
+ case "email":
27096
+ return `email ${condition.operator === "is-any" ? "is one of" : "is none of"} ${condition.emails.join(", ")} (set in the dashboard)`;
26703
27097
  }
26704
27098
  }
26705
27099
  function registerSegmentsCommands(program2) {
@@ -27074,15 +27468,25 @@ function registerSiteCommands(program2) {
27074
27468
  );
27075
27469
  update.command("general").description(
27076
27470
  "Page title/description, language, showPageTitle, mainPage* (undefined = untouched)"
27077
- ).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(
27471
+ ).option("--title <text>", "browser/page title of the site").option("--description <text>", "short description (storefront header + meta fallback)").option(
27472
+ "--seo-description <text>",
27473
+ "meta description for search engines (empty = short description)"
27474
+ ).option(
27475
+ "--robots-rules <text>",
27476
+ String.raw`extra robots.txt lines (User-agent / Allow / Disallow / Crawl-delay); separate lines with \n, or use --json`
27477
+ ).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(
27078
27478
  "--json <fileOrJson>",
27079
- "inline JSON object or .json path \u2014 pageTitle, pageDescription, showPageTitle, language, mainPage, mainPageId, mainPageCommunityId"
27479
+ "inline JSON object or .json path \u2014 pageTitle, pageDescription, seoDescription, robotsTxtRules, showPageTitle, language, mainPage, mainPageId, mainPageCommunityId"
27080
27480
  ).action(
27081
27481
  async (options, command) => {
27082
27482
  const flags = getGlobalFlags(command);
27083
27483
  const body = baseBody(options.json);
27084
27484
  if (options.title !== void 0) body.pageTitle = options.title;
27085
27485
  if (options.description !== void 0) body.pageDescription = options.description;
27486
+ if (options.seoDescription !== void 0) body.seoDescription = options.seoDescription;
27487
+ if (options.robotsRules !== void 0) {
27488
+ body.robotsTxtRules = options.robotsRules.split(String.raw`\n`).join("\n");
27489
+ }
27086
27490
  if (options.language !== void 0) {
27087
27491
  body.language = parseChoice7(options.language, PLATFORM_LANGUAGES, "--language");
27088
27492
  }
@@ -29391,17 +29795,17 @@ var GRAPH_NODE_KINDS = /* @__PURE__ */ new Set([
29391
29795
  ]);
29392
29796
  var GRAPH_CONDITION_BRANCHES = ["true", "false"];
29393
29797
  var EXCERPT_LENGTH = 120;
29394
- function isRecord2(value) {
29798
+ function isRecord3(value) {
29395
29799
  return value !== null && typeof value === "object" && !Array.isArray(value);
29396
29800
  }
29397
29801
  function optionalString(value) {
29398
29802
  return typeof value === "string" ? value : void 0;
29399
29803
  }
29400
29804
  function asAutomationGraph(raw) {
29401
- if (!isRecord2(raw) || !Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) return null;
29805
+ if (!isRecord3(raw) || !Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) return null;
29402
29806
  const nodes = [];
29403
29807
  for (const candidate of raw.nodes) {
29404
- if (!isRecord2(candidate)) continue;
29808
+ if (!isRecord3(candidate)) continue;
29405
29809
  const { id, kind, config, name, disabled } = candidate;
29406
29810
  if (typeof id !== "string" || typeof kind !== "string" || !GRAPH_NODE_KINDS.has(kind)) {
29407
29811
  continue;
@@ -29409,14 +29813,14 @@ function asAutomationGraph(raw) {
29409
29813
  nodes.push({
29410
29814
  id,
29411
29815
  kind,
29412
- config: isRecord2(config) ? config : {},
29816
+ config: isRecord3(config) ? config : {},
29413
29817
  ...typeof name === "string" && name.length > 0 ? { name } : {},
29414
29818
  ...disabled === true ? { disabled: true } : {}
29415
29819
  });
29416
29820
  }
29417
29821
  const edges = [];
29418
29822
  for (const candidate of raw.edges) {
29419
- if (!isRecord2(candidate)) continue;
29823
+ if (!isRecord3(candidate)) continue;
29420
29824
  const { source, target, sourceHandle } = candidate;
29421
29825
  if (typeof source !== "string" || typeof target !== "string") continue;
29422
29826
  const branch = GRAPH_CONDITION_BRANCHES.find((b) => b === sourceHandle);
@@ -29426,7 +29830,7 @@ function asAutomationGraph(raw) {
29426
29830
  }
29427
29831
  function emailConfigOf(node) {
29428
29832
  const raw = node.config.email;
29429
- if (!isRecord2(raw)) return null;
29833
+ if (!isRecord3(raw)) return null;
29430
29834
  return {
29431
29835
  templateId: optionalString(raw.templateId) ?? null,
29432
29836
  templateName: optionalString(raw.templateName) ?? null,
@@ -29578,7 +29982,7 @@ function isKnownActionType(value) {
29578
29982
  function describeAction(node, templateNames) {
29579
29983
  const { taskType } = node.config;
29580
29984
  if (!isKnownActionType(taskType)) {
29581
- const inner = isRecord2(node.config.action) ? optionalString(node.config.action.type) : void 0;
29985
+ const inner = isRecord3(node.config.action) ? optionalString(node.config.action.type) : void 0;
29582
29986
  return `${typeof taskType === "string" ? taskType : "action"}${inner ? ` ${inner}` : ""}`;
29583
29987
  }
29584
29988
  const label = GRAPH_ACTION_LABELS[taskType];
@@ -29586,39 +29990,39 @@ function describeAction(node, templateNames) {
29586
29990
  case "sendEmail":
29587
29991
  return `${label} \u2014 ${describeEmail(node, templateNames)}`;
29588
29992
  case "notifyOwner": {
29589
- const owner = isRecord2(node.config.ownerNotification) ? node.config.ownerNotification : {};
29993
+ const owner = isRecord3(node.config.ownerNotification) ? node.config.ownerNotification : {};
29590
29994
  return `${label} \u2014 subject: ${quote(optionalString(owner.subject))} \u2014 body: ${quote(excerptHtml(optionalString(owner.html)))}`;
29591
29995
  }
29592
29996
  case "sendSms": {
29593
- const sms = isRecord2(node.config.sms) ? node.config.sms : {};
29997
+ const sms = isRecord3(node.config.sms) ? node.config.sms : {};
29594
29998
  return `${label} \u2014 text: ${quote(excerptText(optionalString(sms.text)))}`;
29595
29999
  }
29596
30000
  case "webhook": {
29597
- const webhook = isRecord2(node.config.webhook) ? node.config.webhook : {};
30001
+ const webhook = isRecord3(node.config.webhook) ? node.config.webhook : {};
29598
30002
  const method = optionalString(webhook.method) ?? optionalString(webhook.type);
29599
30003
  return `${label} \u2014 ${method ? `${method.toUpperCase()} ` : ""}${optionalString(webhook.url) ?? "(no url)"}`;
29600
30004
  }
29601
30005
  case "publishSocialPost": {
29602
- const social = isRecord2(node.config.social) ? node.config.social : {};
30006
+ const social = isRecord3(node.config.social) ? node.config.social : {};
29603
30007
  const targets = Array.isArray(social.targets) ? social.targets.join(", ") : "";
29604
30008
  return `${label}${optionalString(social.mode) ? ` (${social.mode})` : ""}${targets ? ` to ${targets}` : ""} \u2014 text: ${quote(excerptText(optionalString(social.text)))}`;
29605
30009
  }
29606
30010
  case "createCommunityPost": {
29607
- const post = isRecord2(node.config.communityPost) ? node.config.communityPost : {};
30011
+ const post = isRecord3(node.config.communityPost) ? node.config.communityPost : {};
29608
30012
  const title = optionalString(post.title);
29609
30013
  return `${label} in channel ${optionalString(post.channelId) ?? "?"}${title ? ` \u2014 title: ${quote(title)}` : ""} \u2014 text: ${quote(excerptText(optionalString(post.text)))}`;
29610
30014
  }
29611
30015
  case "updateClientTags": {
29612
- const tags = isRecord2(node.config.clientTags) ? node.config.clientTags : {};
30016
+ const tags = isRecord3(node.config.clientTags) ? node.config.clientTags : {};
29613
30017
  const ids = Array.isArray(tags.tagIds) ? tags.tagIds.join(", ") : "";
29614
30018
  return `${label} \u2014 ${optionalString(tags.mode) ?? "update"} ${ids || "(no tags)"}`;
29615
30019
  }
29616
30020
  case "setEmailSubscription": {
29617
- const sub = isRecord2(node.config.emailSubscription) ? node.config.emailSubscription : {};
30021
+ const sub = isRecord3(node.config.emailSubscription) ? node.config.emailSubscription : {};
29618
30022
  return `${label} \u2014 ${optionalString(sub.status) ?? "?"}`;
29619
30023
  }
29620
30024
  case "openClientDeadline": {
29621
- const deadline = isRecord2(node.config.clientDeadline) ? node.config.clientDeadline : {};
30025
+ const deadline = isRecord3(node.config.clientDeadline) ? node.config.clientDeadline : {};
29622
30026
  const amount = typeof deadline.amount === "number" ? String(deadline.amount) : "?";
29623
30027
  const endTime = optionalString(deadline.endTime);
29624
30028
  return `${label} \u2014 ${optionalString(deadline.key) ?? "?"} for ${amount} ${optionalString(deadline.unit) ?? "?"}${endTime ? `, closes at ${endTime}` : ""}`;
@@ -29637,7 +30041,7 @@ function describeEmail(node, templateNames) {
29637
30041
  return `subject: ${quote(email.subject)} \u2014 body: ${quote(excerptHtml(email.html))}`;
29638
30042
  }
29639
30043
  function describeCondition2(config) {
29640
- const rules = Array.isArray(config.rules) ? config.rules.filter(isRecord2) : [];
30044
+ const rules = Array.isArray(config.rules) ? config.rules.filter(isRecord3) : [];
29641
30045
  if (rules.length === 0) return "(no rules)";
29642
30046
  return rules.map(describeRule).join(" AND ");
29643
30047
  }
@@ -29662,7 +30066,11 @@ function describeDelay(config) {
29662
30066
  switch (mode) {
29663
30067
  case void 0:
29664
30068
  case "duration":
29665
- return `wait ${typeof config.amount === "number" ? config.amount : "?"} ${optionalString(config.unit) ?? ""}`.trimEnd();
30069
+ return `wait ${typeof config.amount === "number" ? config.amount : "?"} ${optionalString(config.unit) ?? ""}${optionalString(config.atTime) ? `, then at ${config.atTime}` : ""}`.trimEnd();
30070
+ case "untilTime": {
30071
+ const weekdays = Array.isArray(config.weekdays) ? config.weekdays.join(",") : "";
30072
+ return `wait until the next ${optionalString(config.time) ?? "?"}${weekdays ? ` on weekdays ${weekdays}` : ""}${optionalString(config.timezone) ? ` (${config.timezone})` : ""}`;
30073
+ }
29666
30074
  case "untilWeekday":
29667
30075
  return `wait until weekday ${typeof config.weekday === "number" ? config.weekday : "?"} at ${optionalString(config.time) ?? "?"}${optionalString(config.timezone) ? ` (${config.timezone})` : ""}`;
29668
30076
  case "untilDate":
@@ -29690,6 +30098,40 @@ function excerptText(text) {
29690
30098
  }
29691
30099
 
29692
30100
  // src/commands/workspace/index.ts
30101
+ function toLoginRow(profile) {
30102
+ const { credentials } = profile;
30103
+ return {
30104
+ key: profile.key,
30105
+ active: profile.active,
30106
+ workspaceId: credentials.workspaceId ?? null,
30107
+ workspaceName: credentials.workspaceName ?? null,
30108
+ workspaceSlug: credentials.workspaceSlug ?? null,
30109
+ kind: credentials.kind ?? "workspace",
30110
+ userEmail: credentials.userEmail ?? null,
30111
+ source: credentials.source ?? "manual",
30112
+ expiresAt: credentials.expiresAt ?? null
30113
+ };
30114
+ }
30115
+ function printLogins(profiles) {
30116
+ if (profiles.length === 0) {
30117
+ process.stdout.write("No stored logins. Run `zanfia login` (add --workspace <id|slug>).\n");
30118
+ return;
30119
+ }
30120
+ for (const profile of profiles) {
30121
+ const row = toLoginRow(profile);
30122
+ const name = row.workspaceName ?? "(unnamed)";
30123
+ const slug = row.workspaceSlug ? ` [${row.workspaceSlug}]` : "";
30124
+ const member = row.kind === "member" ? " (community member)" : "";
30125
+ const expires = row.expiresAt ? `, expires ${row.expiresAt.slice(0, 10)}` : "";
30126
+ process.stdout.write(
30127
+ `${row.active ? "*" : " "} ${name}${slug}${member} - ${row.workspaceId ?? row.key}${row.userEmail ? `, ${row.userEmail}` : ""}${expires}
30128
+ `
30129
+ );
30130
+ }
30131
+ process.stdout.write(
30132
+ "\n* = active. Switch: `zanfia workspace use <id|slug>`; one command: `--workspace <id|slug>`.\n"
30133
+ );
30134
+ }
29693
30135
  function parseOnOff5(value, flag) {
29694
30136
  const match = ["on", "off"].find((v) => v === value);
29695
30137
  if (!match) throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
@@ -29731,8 +30173,23 @@ function printSettings(settings) {
29731
30173
  }
29732
30174
  function registerWorkspaceCommands(program2) {
29733
30175
  const workspace2 = program2.command("workspace").description(
29734
- 'Workspace-level settings: the name, the scheduling timezone (workflow "wait until" steps and other workspace-scoped scheduling) and the video transcription defaults'
30176
+ "Workspace-level settings (the name, the scheduling timezone and the video transcription defaults) and the workspace logins stored on this device (list, use)"
29735
30177
  );
30178
+ workspace2.command("list").description("List the workspace logins stored on this device (* = active)").action((_options, command) => {
30179
+ const flags = getGlobalFlags(command);
30180
+ const profiles = listStoredProfiles();
30181
+ emit(flags, profiles.map(toLoginRow), () => printLogins(profiles));
30182
+ });
30183
+ 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) => {
30184
+ const flags = getGlobalFlags(command);
30185
+ const profile = useStoredProfile(ref);
30186
+ emit(flags, toLoginRow(profile), () => {
30187
+ process.stdout.write(
30188
+ `Active workspace: ${profile.credentials.workspaceName ?? "?"} (${profile.credentials.workspaceId ?? profile.key})
30189
+ `
30190
+ );
30191
+ });
30192
+ });
29736
30193
  workspace2.command("get", { isDefault: true }).description("Show the workspace settings").action(async (_options, command) => {
29737
30194
  const flags = getGlobalFlags(command);
29738
30195
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -29829,8 +30286,8 @@ function applyShortHelp(program2) {
29829
30286
 
29830
30287
  // src/program.ts
29831
30288
  function resolveCliVersion() {
29832
- if ("0.3.63".length > 0) {
29833
- return "0.3.63";
30289
+ if ("0.6.4".length > 0) {
30290
+ return "0.6.4";
29834
30291
  }
29835
30292
  try {
29836
30293
  const raw = readFileSync28(new URL("../package.json", import.meta.url), "utf8");
@@ -29871,7 +30328,7 @@ Signing in \u2014 two roles, two consent pages:
29871
30328
  registerLoginCommand(program2);
29872
30329
  registerRegisterCommand(program2);
29873
30330
  registerJoinCommand(program2);
29874
- registerLogoutCommand(program2, clearCredentials);
30331
+ registerLogoutCommand(program2);
29875
30332
  registerAuthCommands(program2);
29876
30333
  registerProductsCommands(program2);
29877
30334
  registerOffersCommands(program2);