@zanfia/cli 0.4.0 → 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 +548 -144
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,52 +1,52 @@
1
- # @zanfia/cli (`zanfia`)
2
-
3
- Thin HTTP client over the Zanfia Public API.
4
-
5
- > **Status:** published to npm as `@zanfia/cli` (bin `zanfia`), talking to the production Workspace API by default (`ZANFIA_CLI_TARGET=prod` bakes the URLs at build time). Set `ZANFIA_API_URL` to target another environment; local `tsx` runs without baked defaults fall back to the in-memory mock.
6
-
7
- ## Local usage
8
-
9
- ```bash
10
- # from the monorepo root, after `pnpm install`
11
- pnpm --filter @zanfia/cli dev products list
12
-
13
- # or run a one-off
14
- pnpm --filter @zanfia/cli dev auth whoami
15
-
16
- # global flags (--json, --verbose, --api-key) must come BEFORE the subcommand:
17
- pnpm --filter @zanfia/cli dev --json products list
18
- ```
19
-
20
- ## System emails
21
-
22
- `zanfia emails` manages the customer-facing emails Zanfia sends in your name (Site → Emails):
23
-
24
- ```bash
25
- zanfia emails list # key, group, status, subject
26
- zanfia emails get purchase-confirmation # subject/headline EN+PL, layout blocks, variables
27
- zanfia emails set purchase-confirmation --subject-pl "Dzięki za zakup {{productName}}"
28
- zanfia emails set access-ending-reminder --disable # optional emails only
29
- zanfia emails set login-code --layout ./layout.json # full EmailBlock[] (inline JSON or file)
30
- zanfia emails set login-code --reset # back to the stock email
31
- zanfia emails branding get
32
- zanfia emails branding set --accent-color "#2563eb" --reply-to none
33
- zanfia emails test purchase-confirmation --lang en # queued to YOUR own address
34
- ```
35
-
36
- Raw HTTP: `GET|PATCH /email-templates`, `POST /email-templates/{key}/test-sends`. Full reference for every
37
- command group: `.claude/skills/zanfia-cli/SKILL.md`.
38
-
39
- ## Auth resolution order
40
-
41
- 1. `--api-key` flag
42
- 2. `ZANFIA_API_KEY` environment variable
43
- 3. Stored key in `~/.config/zanfia/credentials.json`
44
- 4. Prompt to run `zanfia auth set-key`
45
-
46
- ## Environment
47
-
48
- | Variable | Purpose |
49
- | ----------------- | ------------------------------------------------ |
50
- | `ZANFIA_API_URL` | Public API base URL. Unset → mock mode. |
51
- | `ZANFIA_API_KEY` | API key (overrides stored credentials). |
52
- | `ZANFIA_CLI_MOCK` | Force mock mode even if `ZANFIA_API_URL` is set. |
1
+ # @zanfia/cli (`zanfia`)
2
+
3
+ Thin HTTP client over the Zanfia Public API.
4
+
5
+ > **Status:** published to npm as `@zanfia/cli` (bin `zanfia`), talking to the production Workspace API by default (`ZANFIA_CLI_TARGET=prod` bakes the URLs at build time). Set `ZANFIA_API_URL` to target another environment; local `tsx` runs without baked defaults fall back to the in-memory mock.
6
+
7
+ ## Local usage
8
+
9
+ ```bash
10
+ # from the monorepo root, after `pnpm install`
11
+ pnpm --filter @zanfia/cli dev products list
12
+
13
+ # or run a one-off
14
+ pnpm --filter @zanfia/cli dev auth whoami
15
+
16
+ # global flags (--json, --verbose, --api-key) must come BEFORE the subcommand:
17
+ pnpm --filter @zanfia/cli dev --json products list
18
+ ```
19
+
20
+ ## System emails
21
+
22
+ `zanfia emails` manages the customer-facing emails Zanfia sends in your name (Site → Emails):
23
+
24
+ ```bash
25
+ zanfia emails list # key, group, status, subject
26
+ zanfia emails get purchase-confirmation # subject/headline EN+PL, layout blocks, variables
27
+ zanfia emails set purchase-confirmation --subject-pl "Dzięki za zakup {{productName}}"
28
+ zanfia emails set access-ending-reminder --disable # optional emails only
29
+ zanfia emails set login-code --layout ./layout.json # full EmailBlock[] (inline JSON or file)
30
+ zanfia emails set login-code --reset # back to the stock email
31
+ zanfia emails branding get
32
+ zanfia emails branding set --accent-color "#2563eb" --reply-to none
33
+ zanfia emails test purchase-confirmation --lang en # queued to YOUR own address
34
+ ```
35
+
36
+ Raw HTTP: `GET|PATCH /email-templates`, `POST /email-templates/{key}/test-sends`. Full reference for every
37
+ command group: `.claude/skills/zanfia-cli/SKILL.md`.
38
+
39
+ ## Auth resolution order
40
+
41
+ 1. `--api-key` flag
42
+ 2. `ZANFIA_API_KEY` environment variable
43
+ 3. Stored key in `~/.config/zanfia/credentials.json`
44
+ 4. Prompt to run `zanfia auth set-key`
45
+
46
+ ## Environment
47
+
48
+ | Variable | Purpose |
49
+ | ----------------- | ------------------------------------------------ |
50
+ | `ZANFIA_API_URL` | Public API base URL. Unset → mock mode. |
51
+ | `ZANFIA_API_KEY` | API key (overrides stored credentials). |
52
+ | `ZANFIA_CLI_MOCK` | Force mock mode even if `ZANFIA_API_URL` is set. |
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,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) {
@@ -11158,16 +11297,93 @@ More ads available \u2014 re-run with --after ${result.after}
11158
11297
  }
11159
11298
 
11160
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
+ }
11161
11365
  function registerAuthCommands(program2) {
11162
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));
11163
11368
  auth.command("status").description("Show what the CLI currently has stored (path, source, expiry, api URL).").action(() => {
11164
11369
  const stored = readCredentials();
11165
11370
  const apiKey = resolveApiKey(void 0);
11166
11371
  const apiUrl = resolveApiUrl();
11167
11372
  const envKey = process.env.ZANFIA_API_KEY ? "(from $ZANFIA_API_KEY)" : "";
11168
11373
  const envUrl = process.env.ZANFIA_API_URL ? "(from $ZANFIA_API_URL)" : "";
11169
- process.stdout.write(`Credentials file: ${credentialsPath}
11374
+ const profiles = listStoredProfiles();
11375
+ process.stdout.write(`Credentials file: ${credentialsFilePath()}
11170
11376
  `);
11377
+ if (profiles.length > 1) {
11378
+ process.stdout.write(`Stored logins: ${profiles.length} (active one below)
11379
+ `);
11380
+ for (const profile of profiles) {
11381
+ process.stdout.write(
11382
+ ` ${profile.active ? "*" : " "} ${profile.credentials.workspaceName ?? "?"} (${profile.key})
11383
+ `
11384
+ );
11385
+ }
11386
+ }
11171
11387
  if (!stored) {
11172
11388
  process.stdout.write(` status: no credentials file found
11173
11389
  `);
@@ -11244,54 +11460,7 @@ Resolved by next call:
11244
11460
  );
11245
11461
  }
11246
11462
  });
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
- });
11463
+ auth.command("whoami").description("Show the authenticated identity (creator workspace, or community member)").action(async (_, command) => runWhoami(command));
11295
11464
  }
11296
11465
 
11297
11466
  // src/commands/auth/login.ts
@@ -11490,25 +11659,54 @@ async function postJson(url, body) {
11490
11659
  }
11491
11660
  return JSON.parse(text);
11492
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
+ }
11493
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) {
11494
11689
  const urls = resolveUrls(options.apiUrl);
11495
11690
  const state = randomBytes(32).toString("base64url");
11496
11691
  const { verifier, challenge } = generatePkce();
11497
11692
  const host = safeHostname();
11498
11693
  const server = await startLoopbackServer();
11499
11694
  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}
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}
11509
11707
  `);
11510
11708
  process.stdout.write("Waiting for browser callback...\n\n");
11511
- const opened = await openBrowser(consentUrl);
11709
+ const opened = await openBrowser(openUrl);
11512
11710
  if (!opened) {
11513
11711
  process.stdout.write("Could not open browser automatically \u2014 paste the URL above.\n\n");
11514
11712
  }
@@ -11532,14 +11730,24 @@ async function runLogin(options) {
11532
11730
  expiresAt: exchanged.expiresAt,
11533
11731
  userEmail: exchanged.userEmail,
11534
11732
  workspaceId: exchanged.workspaceId,
11535
- workspaceName: exchanged.workspaceName
11733
+ workspaceName: exchanged.workspaceName,
11734
+ workspaceSlug: exchanged.workspaceSlug ?? null
11536
11735
  });
11537
11736
  process.stdout.write(`Logged in as ${exchanged.userEmail}.
11538
11737
  `);
11539
- process.stdout.write(`Workspace: ${exchanged.workspaceName} (${exchanged.workspaceId})
11540
- `);
11738
+ process.stdout.write(
11739
+ `Workspace: ${exchanged.workspaceName} (${exchanged.workspaceId}) - now active
11740
+ `
11741
+ );
11541
11742
  process.stdout.write(`Credentials stored at ${stored}
11542
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
+ }
11543
11751
  if (!urls.apiUrl) {
11544
11752
  process.stdout.write(
11545
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"
@@ -11652,6 +11860,9 @@ function registerLoginCommand(auth) {
11652
11860
  auth.command("login").description(
11653
11861
  "Open a browser to authorize the CLI via OAuth (pass --address to sign in as a community member)"
11654
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(
11655
11866
  "--address <url>",
11656
11867
  "sign in as a community member (Community API) against this creator platform URL"
11657
11868
  ).option("-v, --verbose", "show HTTP request details").addHelpText(
@@ -11660,9 +11871,13 @@ function registerLoginCommand(auth) {
11660
11871
  The two flows use different consent pages:
11661
11872
 
11662
11873
  Creator (workspace) login \u2014 full Workspace API:
11663
- login
11874
+ login [--workspace <id|slug>]
11664
11875
  Opens the consent page in the Zanfia dashboard. Sign in with your
11665
- 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.
11666
11881
 
11667
11882
  Community member login \u2014 Community API only:
11668
11883
  login --address https://<creator-platform-url>
@@ -11686,14 +11901,27 @@ The two flows use different consent pages:
11686
11901
  await runLogin(options);
11687
11902
  });
11688
11903
  }
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 {
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) {
11696
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
+ );
11697
11925
  }
11698
11926
  });
11699
11927
  }
@@ -11703,6 +11931,16 @@ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
11703
11931
 
11704
11932
  // src/utils/httpJson.ts
11705
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
+ };
11706
11944
  async function postJson2(label, url, body, options = {}) {
11707
11945
  if (options.verbose) process.stderr.write(`\u2192 POST ${url}
11708
11946
  `);
@@ -11718,8 +11956,10 @@ async function postJson2(label, url, body, options = {}) {
11718
11956
  });
11719
11957
  const text = await response.text();
11720
11958
  if (!response.ok) {
11721
- throw new Error(
11722
- `${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)
11723
11963
  );
11724
11964
  }
11725
11965
  return JSON.parse(text);
@@ -11734,6 +11974,14 @@ function extractErrorMessage(text, fallback) {
11734
11974
  }
11735
11975
  return text || fallback;
11736
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
+ }
11737
11985
  function safeHostname2() {
11738
11986
  try {
11739
11987
  const h = hostname2();
@@ -11790,6 +12038,7 @@ function promptHidden(question) {
11790
12038
  }
11791
12039
 
11792
12040
  // src/commands/auth/register.ts
12041
+ var BOT_CHECK_FAILED_CODE = "auth/bot-check-failed";
11793
12042
  var MAX_CODE_ATTEMPTS = 5;
11794
12043
  var MIN_PASSWORD_LENGTH = 8;
11795
12044
  var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -11833,6 +12082,32 @@ async function runRegister(options) {
11833
12082
  if (!EMAIL_REGEX.test(email)) {
11834
12083
  throw new Error(`"${email}" does not look like a valid email address.`);
11835
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
+ `);
11836
12111
  const fullName = options.name ?? await promptLine("Full name: ");
11837
12112
  if (fullName.length < 3 || fullName.length > 100) {
11838
12113
  throw new Error("Full name must be 3-100 characters.");
@@ -11859,21 +12134,6 @@ async function runRegister(options) {
11859
12134
  } else if (password.length < MIN_PASSWORD_LENGTH) {
11860
12135
  throw new Error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters.`);
11861
12136
  }
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
12137
  let verificationToken = null;
11878
12138
  for (let attempt = 1; attempt <= MAX_CODE_ATTEMPTS; attempt++) {
11879
12139
  const code = options.code ?? await promptLine("Verification code (or 'r' to resend): ");
@@ -11978,16 +12238,18 @@ Logged in as ${exchanged.userEmail}.
11978
12238
  }
11979
12239
  }
11980
12240
  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(
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(
11982
12242
  "--password <password>",
11983
12243
  "optional account password (min 8 chars); prefer the interactive prompt \u2014 flags leak into shell history"
11984
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(
11985
12245
  "after",
11986
12246
  `
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.
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.
11991
12253
 
11992
12254
  Running it with an email that already has a creator account simply signs
11993
12255
  that account in.
@@ -12608,6 +12870,28 @@ function registerCampaignsCommands(program2) {
12608
12870
  const { stats } = result.campaign;
12609
12871
  process.stdout.write(
12610
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\`.
12611
12895
  `
12612
12896
  );
12613
12897
  });
@@ -13756,7 +14040,15 @@ function printMergePreview(preview) {
13756
14040
  }
13757
14041
  function registerClientsCommands(program2) {
13758
14042
  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(
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(
13760
14052
  async (options, command) => {
13761
14053
  const flags = getGlobalFlags(command);
13762
14054
  if (options.withProducts && options.withoutProducts) {
@@ -13767,7 +14059,9 @@ function registerClientsCommands(program2) {
13767
14059
  search: options.search,
13768
14060
  limit: options.limit !== void 0 ? parsePositiveInt4(options.limit, "--limit") : void 0,
13769
14061
  cursor: options.cursor,
13770
- 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
13771
14065
  });
13772
14066
  emit(flags, page, () => {
13773
14067
  printTable(
@@ -19297,9 +19591,14 @@ join again to refresh it.
19297
19591
  // src/commands/liveRooms/index.ts
19298
19592
  var minutes = (seconds) => `${Math.round(seconds / 60)} min`;
19299
19593
  var percent = (part, whole) => whole > 0 ? `${Math.round(part / whole * 100)}%` : "-";
19594
+ var WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
19300
19595
  function describeSchedule(room) {
19301
19596
  if (!room.schedule) return "fixed";
19302
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
+ }
19303
19602
  return `every ${room.schedule.everyMinutes} min${until}`;
19304
19603
  }
19305
19604
  function registerLiveRoomsCommands(program2) {
@@ -25298,6 +25597,10 @@ function registerProductsCommands(program2) {
25298
25597
  `);
25299
25598
  process.stdout.write(` Status: ${product.status}
25300
25599
  `);
25600
+ if (product.storefrontVisibility) {
25601
+ process.stdout.write(` Visibility: ${product.storefrontVisibility}
25602
+ `);
25603
+ }
25301
25604
  process.stdout.write(` Clients: ${product.clientCount}
25302
25605
  `);
25303
25606
  if (product.description) {
@@ -25384,13 +25687,21 @@ function registerProductsCommands(program2) {
25384
25687
  });
25385
25688
  }
25386
25689
  );
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(
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(
25388
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
+ }
25389
25699
  const flags = getGlobalFlags(command);
25390
25700
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
25391
25701
  const result = await client.updateProduct(id, {
25392
25702
  name: options.name,
25393
- description: options.description
25703
+ description: options.description,
25704
+ storefrontVisibility: options.visibility
25394
25705
  });
25395
25706
  emit(flags, result, () => {
25396
25707
  process.stdout.write(`Updated product ${result.productId}
@@ -25407,6 +25718,26 @@ function registerProductsCommands(program2) {
25407
25718
  `);
25408
25719
  });
25409
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
+ });
25410
25741
  products2.command("publish").description("Publish a product (status \u2192 published; live for customers)").argument("<id>", "product id").action(async (id, _, command) => {
25411
25742
  const flags = getGlobalFlags(command);
25412
25743
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -25951,6 +26282,18 @@ function registerProductsCommands(program2) {
25951
26282
  );
25952
26283
  });
25953
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
+ });
25954
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) => {
25955
26298
  const flags = getGlobalFlags(command);
25956
26299
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -26749,6 +27092,8 @@ function describeCondition(condition) {
26749
27092
  return condition.operator === "has" ? "has a phone number" : "has no phone number";
26750
27093
  case "campaign":
26751
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)`;
26752
27097
  }
26753
27098
  }
26754
27099
  function registerSegmentsCommands(program2) {
@@ -27123,15 +27468,25 @@ function registerSiteCommands(program2) {
27123
27468
  );
27124
27469
  update.command("general").description(
27125
27470
  "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(
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(
27127
27478
  "--json <fileOrJson>",
27128
- "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"
27129
27480
  ).action(
27130
27481
  async (options, command) => {
27131
27482
  const flags = getGlobalFlags(command);
27132
27483
  const body = baseBody(options.json);
27133
27484
  if (options.title !== void 0) body.pageTitle = options.title;
27134
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
+ }
27135
27490
  if (options.language !== void 0) {
27136
27491
  body.language = parseChoice7(options.language, PLATFORM_LANGUAGES, "--language");
27137
27492
  }
@@ -29440,17 +29795,17 @@ var GRAPH_NODE_KINDS = /* @__PURE__ */ new Set([
29440
29795
  ]);
29441
29796
  var GRAPH_CONDITION_BRANCHES = ["true", "false"];
29442
29797
  var EXCERPT_LENGTH = 120;
29443
- function isRecord2(value) {
29798
+ function isRecord3(value) {
29444
29799
  return value !== null && typeof value === "object" && !Array.isArray(value);
29445
29800
  }
29446
29801
  function optionalString(value) {
29447
29802
  return typeof value === "string" ? value : void 0;
29448
29803
  }
29449
29804
  function asAutomationGraph(raw) {
29450
- 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;
29451
29806
  const nodes = [];
29452
29807
  for (const candidate of raw.nodes) {
29453
- if (!isRecord2(candidate)) continue;
29808
+ if (!isRecord3(candidate)) continue;
29454
29809
  const { id, kind, config, name, disabled } = candidate;
29455
29810
  if (typeof id !== "string" || typeof kind !== "string" || !GRAPH_NODE_KINDS.has(kind)) {
29456
29811
  continue;
@@ -29458,14 +29813,14 @@ function asAutomationGraph(raw) {
29458
29813
  nodes.push({
29459
29814
  id,
29460
29815
  kind,
29461
- config: isRecord2(config) ? config : {},
29816
+ config: isRecord3(config) ? config : {},
29462
29817
  ...typeof name === "string" && name.length > 0 ? { name } : {},
29463
29818
  ...disabled === true ? { disabled: true } : {}
29464
29819
  });
29465
29820
  }
29466
29821
  const edges = [];
29467
29822
  for (const candidate of raw.edges) {
29468
- if (!isRecord2(candidate)) continue;
29823
+ if (!isRecord3(candidate)) continue;
29469
29824
  const { source, target, sourceHandle } = candidate;
29470
29825
  if (typeof source !== "string" || typeof target !== "string") continue;
29471
29826
  const branch = GRAPH_CONDITION_BRANCHES.find((b) => b === sourceHandle);
@@ -29475,7 +29830,7 @@ function asAutomationGraph(raw) {
29475
29830
  }
29476
29831
  function emailConfigOf(node) {
29477
29832
  const raw = node.config.email;
29478
- if (!isRecord2(raw)) return null;
29833
+ if (!isRecord3(raw)) return null;
29479
29834
  return {
29480
29835
  templateId: optionalString(raw.templateId) ?? null,
29481
29836
  templateName: optionalString(raw.templateName) ?? null,
@@ -29627,7 +29982,7 @@ function isKnownActionType(value) {
29627
29982
  function describeAction(node, templateNames) {
29628
29983
  const { taskType } = node.config;
29629
29984
  if (!isKnownActionType(taskType)) {
29630
- 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;
29631
29986
  return `${typeof taskType === "string" ? taskType : "action"}${inner ? ` ${inner}` : ""}`;
29632
29987
  }
29633
29988
  const label = GRAPH_ACTION_LABELS[taskType];
@@ -29635,39 +29990,39 @@ function describeAction(node, templateNames) {
29635
29990
  case "sendEmail":
29636
29991
  return `${label} \u2014 ${describeEmail(node, templateNames)}`;
29637
29992
  case "notifyOwner": {
29638
- const owner = isRecord2(node.config.ownerNotification) ? node.config.ownerNotification : {};
29993
+ const owner = isRecord3(node.config.ownerNotification) ? node.config.ownerNotification : {};
29639
29994
  return `${label} \u2014 subject: ${quote(optionalString(owner.subject))} \u2014 body: ${quote(excerptHtml(optionalString(owner.html)))}`;
29640
29995
  }
29641
29996
  case "sendSms": {
29642
- const sms = isRecord2(node.config.sms) ? node.config.sms : {};
29997
+ const sms = isRecord3(node.config.sms) ? node.config.sms : {};
29643
29998
  return `${label} \u2014 text: ${quote(excerptText(optionalString(sms.text)))}`;
29644
29999
  }
29645
30000
  case "webhook": {
29646
- const webhook = isRecord2(node.config.webhook) ? node.config.webhook : {};
30001
+ const webhook = isRecord3(node.config.webhook) ? node.config.webhook : {};
29647
30002
  const method = optionalString(webhook.method) ?? optionalString(webhook.type);
29648
30003
  return `${label} \u2014 ${method ? `${method.toUpperCase()} ` : ""}${optionalString(webhook.url) ?? "(no url)"}`;
29649
30004
  }
29650
30005
  case "publishSocialPost": {
29651
- const social = isRecord2(node.config.social) ? node.config.social : {};
30006
+ const social = isRecord3(node.config.social) ? node.config.social : {};
29652
30007
  const targets = Array.isArray(social.targets) ? social.targets.join(", ") : "";
29653
30008
  return `${label}${optionalString(social.mode) ? ` (${social.mode})` : ""}${targets ? ` to ${targets}` : ""} \u2014 text: ${quote(excerptText(optionalString(social.text)))}`;
29654
30009
  }
29655
30010
  case "createCommunityPost": {
29656
- const post = isRecord2(node.config.communityPost) ? node.config.communityPost : {};
30011
+ const post = isRecord3(node.config.communityPost) ? node.config.communityPost : {};
29657
30012
  const title = optionalString(post.title);
29658
30013
  return `${label} in channel ${optionalString(post.channelId) ?? "?"}${title ? ` \u2014 title: ${quote(title)}` : ""} \u2014 text: ${quote(excerptText(optionalString(post.text)))}`;
29659
30014
  }
29660
30015
  case "updateClientTags": {
29661
- const tags = isRecord2(node.config.clientTags) ? node.config.clientTags : {};
30016
+ const tags = isRecord3(node.config.clientTags) ? node.config.clientTags : {};
29662
30017
  const ids = Array.isArray(tags.tagIds) ? tags.tagIds.join(", ") : "";
29663
30018
  return `${label} \u2014 ${optionalString(tags.mode) ?? "update"} ${ids || "(no tags)"}`;
29664
30019
  }
29665
30020
  case "setEmailSubscription": {
29666
- const sub = isRecord2(node.config.emailSubscription) ? node.config.emailSubscription : {};
30021
+ const sub = isRecord3(node.config.emailSubscription) ? node.config.emailSubscription : {};
29667
30022
  return `${label} \u2014 ${optionalString(sub.status) ?? "?"}`;
29668
30023
  }
29669
30024
  case "openClientDeadline": {
29670
- const deadline = isRecord2(node.config.clientDeadline) ? node.config.clientDeadline : {};
30025
+ const deadline = isRecord3(node.config.clientDeadline) ? node.config.clientDeadline : {};
29671
30026
  const amount = typeof deadline.amount === "number" ? String(deadline.amount) : "?";
29672
30027
  const endTime = optionalString(deadline.endTime);
29673
30028
  return `${label} \u2014 ${optionalString(deadline.key) ?? "?"} for ${amount} ${optionalString(deadline.unit) ?? "?"}${endTime ? `, closes at ${endTime}` : ""}`;
@@ -29686,7 +30041,7 @@ function describeEmail(node, templateNames) {
29686
30041
  return `subject: ${quote(email.subject)} \u2014 body: ${quote(excerptHtml(email.html))}`;
29687
30042
  }
29688
30043
  function describeCondition2(config) {
29689
- const rules = Array.isArray(config.rules) ? config.rules.filter(isRecord2) : [];
30044
+ const rules = Array.isArray(config.rules) ? config.rules.filter(isRecord3) : [];
29690
30045
  if (rules.length === 0) return "(no rules)";
29691
30046
  return rules.map(describeRule).join(" AND ");
29692
30047
  }
@@ -29743,6 +30098,40 @@ function excerptText(text) {
29743
30098
  }
29744
30099
 
29745
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
+ }
29746
30135
  function parseOnOff5(value, flag) {
29747
30136
  const match = ["on", "off"].find((v) => v === value);
29748
30137
  if (!match) throw new Error(`Invalid ${flag}: ${value} (expected on | off)`);
@@ -29784,8 +30173,23 @@ function printSettings(settings) {
29784
30173
  }
29785
30174
  function registerWorkspaceCommands(program2) {
29786
30175
  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'
30176
+ "Workspace-level settings (the name, the scheduling timezone and the video transcription defaults) and the workspace logins stored on this device (list, use)"
29788
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
+ });
29789
30193
  workspace2.command("get", { isDefault: true }).description("Show the workspace settings").action(async (_options, command) => {
29790
30194
  const flags = getGlobalFlags(command);
29791
30195
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -29882,8 +30286,8 @@ function applyShortHelp(program2) {
29882
30286
 
29883
30287
  // src/program.ts
29884
30288
  function resolveCliVersion() {
29885
- if ("0.4.0".length > 0) {
29886
- return "0.4.0";
30289
+ if ("0.6.4".length > 0) {
30290
+ return "0.6.4";
29887
30291
  }
29888
30292
  try {
29889
30293
  const raw = readFileSync28(new URL("../package.json", import.meta.url), "utf8");
@@ -29924,7 +30328,7 @@ Signing in \u2014 two roles, two consent pages:
29924
30328
  registerLoginCommand(program2);
29925
30329
  registerRegisterCommand(program2);
29926
30330
  registerJoinCommand(program2);
29927
- registerLogoutCommand(program2, clearCredentials);
30331
+ registerLogoutCommand(program2);
29928
30332
  registerAuthCommands(program2);
29929
30333
  registerProductsCommands(program2);
29930
30334
  registerOffersCommands(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zanfia/cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.4",
4
4
  "private": false,
5
5
  "description": "Zanfia CLI — thin HTTP client over the Zanfia Public API",
6
6
  "type": "module",