@zanfia/cli 0.2.0 → 0.3.10

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 +33 -33
  2. package/dist/index.js +271 -42
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,33 +1,33 @@
1
- # @zanfia/cli (`zanfia`)
2
-
3
- Thin HTTP client over the Zanfia Public API.
4
-
5
- > **Status:** scaffolded. Until the Public API service (`api-service`) is deployed, commands run against a local in-memory mock. Set `ZANFIA_API_URL` to point at a real service, or leave it unset to use the 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
- ## Auth resolution order
21
-
22
- 1. `--api-key` flag
23
- 2. `ZANFIA_API_KEY` environment variable
24
- 3. Stored key in `~/.config/zanfia/credentials.json`
25
- 4. Prompt to run `zanfia auth set-key`
26
-
27
- ## Environment
28
-
29
- | Variable | Purpose |
30
- |---|---|
31
- | `ZANFIA_API_URL` | Public API base URL. Unset → mock mode. |
32
- | `ZANFIA_API_KEY` | API key (overrides stored credentials). |
33
- | `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
+ ## Auth resolution order
21
+
22
+ 1. `--api-key` flag
23
+ 2. `ZANFIA_API_KEY` environment variable
24
+ 3. Stored key in `~/.config/zanfia/credentials.json`
25
+ 4. Prompt to run `zanfia auth set-key`
26
+
27
+ ## Environment
28
+
29
+ | Variable | Purpose |
30
+ | ----------------- | ------------------------------------------------ |
31
+ | `ZANFIA_API_URL` | Public API base URL. Unset → mock mode. |
32
+ | `ZANFIA_API_KEY` | API key (overrides stored credentials). |
33
+ | `ZANFIA_CLI_MOCK` | Force mock mode even if `ZANFIA_API_URL` is set. |
package/dist/index.js CHANGED
@@ -6908,7 +6908,8 @@ var CONFIG_DIR = join(homedir(), ".config", "zanfia");
6908
6908
  var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
6909
6909
  var orUndefined = (value) => value.length > 0 ? value : void 0;
6910
6910
  var backendDefaults = {
6911
- apiUrl: true ? orUndefined("https://europe-central2-mailingr-54736.cloudfunctions.net/api-v1") : void 0,
6911
+ apiUrl: true ? orUndefined("https://api.zanfia.com") : void 0,
6912
+ communityApiUrl: true ? orUndefined("https://api.zanfia.com/community") : void 0,
6912
6913
  loginUrl: true ? orUndefined("https://app.zanfia.com/cli/auth") : void 0,
6913
6914
  exchangeUrl: true ? orUndefined("https://europe-central2-mailingr-54736.cloudfunctions.net/users-v1/cli/auth/exchange") : void 0,
6914
6915
  memberExchangeUrl: true ? orUndefined("https://europe-central2-mailingr-54736.cloudfunctions.net/users-v1/member/cli/auth/exchange") : void 0,
@@ -6974,7 +6975,7 @@ function resolveCommunityApiUrl() {
6974
6975
  if (!apiUrl) return null;
6975
6976
  if (/\/api-community\/?$/.test(apiUrl)) return apiUrl.replace(/\/$/, "");
6976
6977
  if (/\/api-v1\/?$/.test(apiUrl)) return apiUrl.replace(/\/api-v1\/?$/, "/api-community");
6977
- return null;
6978
+ return backendDefaults.communityApiUrl ?? null;
6978
6979
  }
6979
6980
  function isMockMode() {
6980
6981
  if (process.env.ZANFIA_CLI_MOCK === "1" || process.env.ZANFIA_CLI_MOCK === "true") {
@@ -7086,6 +7087,11 @@ var clients = [
7086
7087
  totalSpentCents: 5800,
7087
7088
  activeProducts: 1,
7088
7089
  language: "en",
7090
+ discordId: null,
7091
+ githubId: null,
7092
+ tagIds: [],
7093
+ emailSubscriptionStatus: null,
7094
+ attribution: null,
7089
7095
  createdAt: "2026-03-02T09:00:00.000Z"
7090
7096
  },
7091
7097
  {
@@ -7098,6 +7104,11 @@ var clients = [
7098
7104
  totalSpentCents: 0,
7099
7105
  activeProducts: 0,
7100
7106
  language: null,
7107
+ discordId: null,
7108
+ githubId: null,
7109
+ tagIds: [],
7110
+ emailSubscriptionStatus: null,
7111
+ attribution: null,
7101
7112
  createdAt: "2026-03-05T09:00:00.000Z"
7102
7113
  }
7103
7114
  ];
@@ -7324,6 +7335,11 @@ var mockBackend = {
7324
7335
  totalSpentCents: 0,
7325
7336
  activeProducts: 0,
7326
7337
  language: input.language ?? null,
7338
+ discordId: null,
7339
+ githubId: null,
7340
+ tagIds: [],
7341
+ emailSubscriptionStatus: null,
7342
+ attribution: null,
7327
7343
  createdAt: now()
7328
7344
  });
7329
7345
  return { clientId: id, created: true, createdUser: true };
@@ -7786,6 +7802,9 @@ function createMockClient() {
7786
7802
  async updateCourseLesson(_productId, lessonId, input) {
7787
7803
  return mockBackend.updateCourseLesson(lessonId, input);
7788
7804
  },
7805
+ async setCourseLessonThumbnail() {
7806
+ throw new ApiError(501, "lesson thumbnails are not available in mock mode");
7807
+ },
7789
7808
  async addCourseModule(_productId, input) {
7790
7809
  return mockBackend.addCourseModule(input);
7791
7810
  },
@@ -8554,6 +8573,11 @@ function createHttpClient(config) {
8554
8573
  `/products/${encodeURIComponent(productId)}/course/lessons/${encodeURIComponent(lessonId)}`,
8555
8574
  input
8556
8575
  ),
8576
+ setCourseLessonThumbnail: (productId, lessonId, input) => request(
8577
+ "PUT",
8578
+ `/products/${encodeURIComponent(productId)}/course/lessons/${encodeURIComponent(lessonId)}/thumbnail`,
8579
+ input
8580
+ ),
8557
8581
  addCourseModule: (productId, input) => request("POST", `/products/${encodeURIComponent(productId)}/course/modules`, input),
8558
8582
  updateCourseModule: (productId, moduleId, input) => request(
8559
8583
  "PATCH",
@@ -9599,38 +9623,9 @@ function registerLogoutCommand(auth, clear) {
9599
9623
 
9600
9624
  // src/commands/auth/register.ts
9601
9625
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
9626
+
9627
+ // src/utils/httpJson.ts
9602
9628
  import { hostname as hostname2 } from "os";
9603
- import { createInterface } from "readline/promises";
9604
- var MAX_CODE_ATTEMPTS = 5;
9605
- var MIN_PASSWORD_LENGTH = 8;
9606
- var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9607
- var CTRL_C = String.fromCharCode(3);
9608
- var DEL = String.fromCharCode(127);
9609
- function resolveRegisterUrls(explicitApiUrl) {
9610
- const exchangeUrl = process.env.ZANFIA_EXCHANGE_URL ?? backendDefaults.exchangeUrl;
9611
- let usersUrl = process.env.ZANFIA_USERS_URL ?? backendDefaults.usersUrl;
9612
- if (!usersUrl && exchangeUrl?.includes("/cli/auth/exchange")) {
9613
- usersUrl = exchangeUrl.replace(/\/cli\/auth\/exchange\/?$/, "");
9614
- }
9615
- if (!usersUrl) {
9616
- throw new Error(
9617
- "ZANFIA_USERS_URL is not set. Set it to the users-service base URL (e.g. https://europe-central2-<project>.cloudfunctions.net/users-v1)."
9618
- );
9619
- }
9620
- usersUrl = usersUrl.replace(/\/$/, "");
9621
- const firebaseApiKey = process.env.ZANFIA_FIREBASE_API_KEY ?? backendDefaults.firebaseApiKey;
9622
- if (!firebaseApiKey) {
9623
- throw new Error(
9624
- "ZANFIA_FIREBASE_API_KEY is not set. Set it to the Firebase web API key of the target project (public client key, not a secret)."
9625
- );
9626
- }
9627
- return {
9628
- usersUrl,
9629
- exchangeUrl: exchangeUrl ?? `${usersUrl}/cli/auth/exchange`,
9630
- firebaseApiKey,
9631
- apiUrl: explicitApiUrl ?? process.env.ZANFIA_API_URL ?? backendDefaults.apiUrl
9632
- };
9633
- }
9634
9629
  async function postJson2(label, url, body, options = {}) {
9635
9630
  if (options.verbose) process.stderr.write(`\u2192 POST ${url}
9636
9631
  `);
@@ -9662,6 +9657,19 @@ function extractErrorMessage(text, fallback) {
9662
9657
  }
9663
9658
  return text || fallback;
9664
9659
  }
9660
+ function safeHostname2() {
9661
+ try {
9662
+ const h = hostname2();
9663
+ return h && h.length > 0 ? h : "unknown";
9664
+ } catch {
9665
+ return "unknown";
9666
+ }
9667
+ }
9668
+
9669
+ // src/utils/prompt.ts
9670
+ import { createInterface } from "readline/promises";
9671
+ var CTRL_C = String.fromCharCode(3);
9672
+ var DEL = String.fromCharCode(127);
9665
9673
  async function promptLine(question) {
9666
9674
  const rl = createInterface({ input: process.stdin, output: process.stdout });
9667
9675
  try {
@@ -9703,13 +9711,35 @@ function promptHidden(question) {
9703
9711
  stdin.on("data", onData);
9704
9712
  });
9705
9713
  }
9706
- function safeHostname2() {
9707
- try {
9708
- const h = hostname2();
9709
- return h && h.length > 0 ? h : "unknown";
9710
- } catch {
9711
- return "unknown";
9714
+
9715
+ // src/commands/auth/register.ts
9716
+ var MAX_CODE_ATTEMPTS = 5;
9717
+ var MIN_PASSWORD_LENGTH = 8;
9718
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9719
+ function resolveRegisterUrls(explicitApiUrl) {
9720
+ const exchangeUrl = process.env.ZANFIA_EXCHANGE_URL ?? backendDefaults.exchangeUrl;
9721
+ let usersUrl = process.env.ZANFIA_USERS_URL ?? backendDefaults.usersUrl;
9722
+ if (!usersUrl && exchangeUrl?.includes("/cli/auth/exchange")) {
9723
+ usersUrl = exchangeUrl.replace(/\/cli\/auth\/exchange\/?$/, "");
9724
+ }
9725
+ if (!usersUrl) {
9726
+ throw new Error(
9727
+ "ZANFIA_USERS_URL is not set. Set it to the users-service base URL (e.g. https://europe-central2-<project>.cloudfunctions.net/users-v1)."
9728
+ );
9729
+ }
9730
+ usersUrl = usersUrl.replace(/\/$/, "");
9731
+ const firebaseApiKey = process.env.ZANFIA_FIREBASE_API_KEY ?? backendDefaults.firebaseApiKey;
9732
+ if (!firebaseApiKey) {
9733
+ throw new Error(
9734
+ "ZANFIA_FIREBASE_API_KEY is not set. Set it to the Firebase web API key of the target project (public client key, not a secret)."
9735
+ );
9712
9736
  }
9737
+ return {
9738
+ usersUrl,
9739
+ exchangeUrl: exchangeUrl ?? `${usersUrl}/cli/auth/exchange`,
9740
+ firebaseApiKey,
9741
+ apiUrl: explicitApiUrl ?? process.env.ZANFIA_API_URL ?? backendDefaults.apiUrl
9742
+ };
9713
9743
  }
9714
9744
  function resolveTimezone() {
9715
9745
  try {
@@ -12459,6 +12489,31 @@ function registerCoursesCommands(program3) {
12459
12489
  });
12460
12490
  }
12461
12491
  );
12492
+ lesson.command("set-thumbnail").description(
12493
+ "Set or clear the lesson thumbnail; Bunny video lessons also get it as the player thumbnail"
12494
+ ).argument("<productId>", "course product id").argument("<lessonId>", "lesson id").option("--media <mediaId>", "media-library IMAGE id (from `media upload`)").option("--url <httpsUrl>", "already-hosted https image URL").option("--clear", "remove the thumbnail (video lessons fall back to the generated frame)").action(
12495
+ async (productId, lessonId, options, command) => {
12496
+ const flags = getGlobalFlags(command);
12497
+ const picked = [options.media, options.url, options.clear].filter(
12498
+ (value) => value !== void 0 && value !== false
12499
+ );
12500
+ if (picked.length !== 1) {
12501
+ throw new Error("Pass exactly one of --media <mediaId>, --url <httpsUrl>, --clear.");
12502
+ }
12503
+ const result = await clientFor2(flags).setCourseLessonThumbnail(productId, lessonId, {
12504
+ ...options.media !== void 0 ? { mediaId: options.media } : {},
12505
+ ...options.url !== void 0 ? { thumbnailUrl: options.url } : {},
12506
+ ...options.clear ? { thumbnailUrl: null } : {}
12507
+ });
12508
+ emit(flags, result, () => {
12509
+ process.stdout.write(
12510
+ result.thumbnailUrl ? `Set thumbnail for lesson ${result.lessonId}: ${result.thumbnailUrl}
12511
+ ` : `Cleared thumbnail for lesson ${result.lessonId}
12512
+ `
12513
+ );
12514
+ });
12515
+ }
12516
+ );
12462
12517
  lesson.command("update").description("Update a lesson (title, description, status, Markdown content)").argument("<productId>", "course product id").argument("<lessonId>", "lesson id").option("--title <title>", "new lesson title").option("--description <text>", "new lesson description").option("--status <status>", `new status (${LESSON_STATUSES.join("|")})`).option("--content <markdown>", "new lesson body (Markdown)").option("--content-file <path>", "read the new lesson body (Markdown) from a file").action(
12463
12518
  async (productId, lessonId, options, command) => {
12464
12519
  const flags = getGlobalFlags(command);
@@ -13341,6 +13396,145 @@ function registerFunnelsCommands(program3) {
13341
13396
  );
13342
13397
  }
13343
13398
 
13399
+ // src/commands/join/index.ts
13400
+ var MAX_CODE_ATTEMPTS2 = 5;
13401
+ var HANDLE_REGEX = /^[a-z0-9][a-z0-9-]{1,62}$/;
13402
+ var EMAIL_REGEX2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
13403
+ var AGENT_DOCS_URL = "https://zanfia.com/help/developer-cli/agents";
13404
+ function joinUrl(base, handle, step) {
13405
+ return `${base.replace(/\/$/, "")}/v1/join/${encodeURIComponent(handle)}/${step}`;
13406
+ }
13407
+ async function runJoin(handleInput, options) {
13408
+ const handle = handleInput.trim().toLowerCase();
13409
+ if (!HANDLE_REGEX.test(handle)) {
13410
+ throw new Error(
13411
+ "Community handle must be lowercase letters, digits and dashes (e.g. crew-of-one)."
13412
+ );
13413
+ }
13414
+ const communityApiUrl = resolveCommunityApiUrl();
13415
+ if (!communityApiUrl) {
13416
+ throw new Error(
13417
+ "No Community API URL configured. Set ZANFIA_COMMUNITY_API_URL (this build has no baked backend)."
13418
+ );
13419
+ }
13420
+ const verbose = options.verbose === true;
13421
+ const email = (options.email ?? await promptLine("Email: ")).trim().toLowerCase();
13422
+ if (!EMAIL_REGEX2.test(email)) {
13423
+ throw new Error("Please enter a valid email address.");
13424
+ }
13425
+ const started = await postJson2(
13426
+ "Join",
13427
+ joinUrl(communityApiUrl, handle, "start"),
13428
+ { email, lang: "en" },
13429
+ { verbose }
13430
+ );
13431
+ if (!options.json) {
13432
+ process.stdout.write(
13433
+ `Joining ${started.communityName} (${started.workspaceName}) \u2014 a 6-digit code was sent to ${email}.
13434
+ `
13435
+ );
13436
+ }
13437
+ const firstName = options.name ?? (options.json ? "" : await promptLine("Your name (optional): "));
13438
+ const hostname3 = safeHostname2();
13439
+ let completed = null;
13440
+ for (let attempt = 1; attempt <= MAX_CODE_ATTEMPTS2 && !completed; attempt += 1) {
13441
+ const code = options.code ?? (await promptLine("Code from the email (or 'r' to resend): ")).trim();
13442
+ if (code.toLowerCase() === "r") {
13443
+ await postJson2(
13444
+ "Resend",
13445
+ joinUrl(communityApiUrl, handle, "start"),
13446
+ { email, lang: "en" },
13447
+ { verbose }
13448
+ );
13449
+ process.stdout.write("Code re-sent.\n");
13450
+ attempt -= 1;
13451
+ continue;
13452
+ }
13453
+ try {
13454
+ completed = await postJson2(
13455
+ "Join",
13456
+ joinUrl(communityApiUrl, handle, "complete"),
13457
+ { email, code, firstName, hostname: hostname3 },
13458
+ { verbose }
13459
+ );
13460
+ } catch (err) {
13461
+ if (options.code || attempt === MAX_CODE_ATTEMPTS2) throw err;
13462
+ process.stdout.write(`${err instanceof Error ? err.message : String(err)}
13463
+ `);
13464
+ }
13465
+ }
13466
+ if (!completed) {
13467
+ throw new Error("Could not verify the code.");
13468
+ }
13469
+ const path2 = writeCredentials({
13470
+ apiKey: completed.token,
13471
+ apiUrl: communityApiUrl,
13472
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
13473
+ source: "oauth",
13474
+ expiresAt: completed.expiresAt,
13475
+ userEmail: completed.userEmail,
13476
+ workspaceId: completed.workspaceId,
13477
+ workspaceName: completed.workspaceName,
13478
+ kind: "member"
13479
+ });
13480
+ if (options.json) {
13481
+ process.stdout.write(
13482
+ `${JSON.stringify(
13483
+ {
13484
+ joined: true,
13485
+ alreadyMember: completed.alreadyMember,
13486
+ communityId: completed.communityId,
13487
+ communityName: completed.communityName,
13488
+ workspaceId: completed.workspaceId,
13489
+ workspaceName: completed.workspaceName,
13490
+ introChannelId: completed.introChannelId,
13491
+ userEmail: completed.userEmail,
13492
+ expiresAt: completed.expiresAt,
13493
+ credentialsPath: path2
13494
+ },
13495
+ null,
13496
+ 2
13497
+ )}
13498
+ `
13499
+ );
13500
+ return;
13501
+ }
13502
+ const intro = completed.introChannelId ? ` zanfia communities post create ${completed.communityId} ${completed.introChannelId} --content "Hi, I'm <your agent>. I ..."
13503
+ ` : "";
13504
+ process.stdout.write(
13505
+ `
13506
+ ${completed.alreadyMember ? "Welcome back to" : "You are in:"} ${completed.communityName}.
13507
+ Member key saved to ${path2} (expires ${completed.expiresAt.slice(0, 10)}).
13508
+
13509
+ Next:
13510
+ zanfia communities list
13511
+ zanfia communities channels ${completed.communityId}
13512
+ ` + (intro ? `
13513
+ Introduce your agent:
13514
+ ${intro}` : "") + `
13515
+ Plug in your agent: ${AGENT_DOCS_URL}
13516
+ `
13517
+ );
13518
+ }
13519
+ function registerJoinCommand(program3) {
13520
+ program3.command("join <handle>").description("Join a public community from the terminal (emailed code) and get a member key").option("--email <email>", "email to join with (prompted when omitted)").option("--name <name>", "your display name (prompted when omitted)").option("--code <code>", "the 6-digit code from the email (prompted when omitted)").addHelpText(
13521
+ "after",
13522
+ `
13523
+ Examples:
13524
+ zanfia join crew-of-one
13525
+ zanfia join crew-of-one --email me@example.com --name "Sam" --code 123456
13526
+
13527
+ The key is a MEMBER key: it reaches the Community API for that creator's
13528
+ communities only (read/post/comment/chat), never your own workspace. It is
13529
+ saved to ~/.config/zanfia/credentials.json and expires in 90 days \u2014 run
13530
+ join again to refresh it.
13531
+ `
13532
+ ).action(async (handle, options, command) => {
13533
+ const globals = command.optsWithGlobals();
13534
+ await runJoin(handle, { ...options, json: globals.json, verbose: globals.verbose });
13535
+ });
13536
+ }
13537
+
13344
13538
  // src/commands/media/index.ts
13345
13539
  import { writeFileSync as writeFileSync2 } from "fs";
13346
13540
  import { readFile, stat as stat3 } from "fs/promises";
@@ -17025,6 +17219,7 @@ function buildPriceInput(options) {
17025
17219
  compareAtAmountCents: options.compareAt ? parsePositiveInt7(options.compareAt, "--compare-at") : void 0,
17026
17220
  accessInterval: options.accessInterval ? parseChoice3(options.accessInterval, PRICE_INTERVALS, "--access-interval") : void 0,
17027
17221
  accessIntervalCount: options.accessCount ? parsePositiveInt7(options.accessCount, "--access-count") : void 0,
17222
+ accessUntil: options.accessUntil || void 0,
17028
17223
  planId: options.plan
17029
17224
  };
17030
17225
  }
@@ -17035,6 +17230,7 @@ var PRICE_PLACEMENT_CHOICES = [
17035
17230
  "belowDescription",
17036
17231
  "aboveDescription"
17037
17232
  ];
17233
+ var PLAN_INCLUDES_LAYOUT_CHOICES = ["inline", "list"];
17038
17234
  var JSON_PATCH_REJECTED_KEYS2 = [
17039
17235
  "active",
17040
17236
  "archived",
@@ -17084,8 +17280,22 @@ function buildOfferUpdateInput(options) {
17084
17280
  options.checkoutMediaWidth === void 0 ? void 0 : options.checkoutMediaWidth.trim().toLowerCase() === "none" ? null : parsePositiveInt7(options.checkoutMediaWidth, "--checkout-media-width")
17085
17281
  );
17086
17282
  set("showTestimonials", bool(options.showTestimonials, "--show-testimonials"));
17283
+ set("showPlanIncludes", bool(options.showPlanIncludes, "--show-plan-includes"));
17284
+ set(
17285
+ "planIncludesLayout",
17286
+ options.planIncludesLayout === void 0 ? void 0 : parseChoice3(
17287
+ options.planIncludesLayout,
17288
+ PLAN_INCLUDES_LAYOUT_CHOICES,
17289
+ "--plan-includes-layout"
17290
+ )
17291
+ );
17087
17292
  set("invoiceAsOption", bool(options.invoiceAsOption, "--invoice-as-option"));
17088
17293
  set("hideCountryField", bool(options.hideCountryField, "--hide-country-field"));
17294
+ set(
17295
+ "fixedCountry",
17296
+ options.fixedCountry === void 0 ? void 0 : options.fixedCountry === "none" ? null : options.fixedCountry.toUpperCase()
17297
+ );
17298
+ set("showFixedCountry", bool(options.showFixedCountry, "--show-fixed-country"));
17089
17299
  set(
17090
17300
  "showPhoneField",
17091
17301
  options.showPhoneField === void 0 ? void 0 : parseVisibility(options.showPhoneField, "--show-phone-field")
@@ -17298,7 +17508,19 @@ Order bumps (${checkout.orderBumps.length}):
17298
17508
  );
17299
17509
  offers.command("update").description(
17300
17510
  'Update any offer (checkout-builder) setting \u2014 display, form & UX, legal, invoicing, notifications, tracking. Boolean flags take on|off; "none" clears a nullable field. Deep structured fields (theme, deadline, afterPurchase, paymentMethodsDisplay, customInvoicesClient, multiplePurchaseType, checkoutImages, checkoutMedia, termsContent, privacyPolicyContent, oto) go through --json. Lifecycle stays with publish/unpublish/delete; order bumps with `bumps attach`/`bumps detach`; the reusable OTO offer documents themselves with `oto create`/`update`/`delete`.'
17301
- ).argument("<checkoutId>", "offer (checkout) id").option("--name <name>", "internal offer name").option("--slug <slug>", "public slug (changing it on a live offer breaks links)").option("--title <title>", "checkout page title").option("--description <description>", "checkout page description").option("--success-url <url|none>", "post-purchase redirect URL (none = clear)").option("--language <code|none>", "checkout page language, e.g. en, pl (none = clear)").option("--checkout-media-width <px|none>", "max media gallery width in px (none = full width)").option("--show-testimonials <on|off>", "show the product testimonials gallery").option("--invoice-as-option <on|off>", "let the buyer opt into invoice data entry").option("--hide-country-field <on|off>", "hide the country field on the form").option("--show-phone-field <off|optional|required>", "phone field visibility").option("--show-additional-info <off|optional|required>", "additional-info field visibility").option("--collect-first-name <on|off>", "free checkouts: collect first name").option("--collect-last-name <on|off>", "free checkouts: collect last name").option(
17511
+ ).argument("<checkoutId>", "offer (checkout) id").option("--name <name>", "internal offer name").option("--slug <slug>", "public slug (changing it on a live offer breaks links)").option("--title <title>", "checkout page title").option("--description <description>", "checkout page description").option("--success-url <url|none>", "post-purchase redirect URL (none = clear)").option("--language <code|none>", "checkout page language, e.g. en, pl (none = clear)").option("--checkout-media-width <px|none>", "max media gallery width in px (none = full width)").option("--show-testimonials <on|off>", "show the product testimonials gallery").option(
17512
+ "--show-plan-includes <on|off>",
17513
+ 'show the "Includes" list of bundled products under each price (multi-plan products; default on)'
17514
+ ).option(
17515
+ "--plan-includes-layout <inline|list>",
17516
+ 'layout of that "Includes" list: inline = one line (default), list = one product per line'
17517
+ ).option("--invoice-as-option <on|off>", "let the buyer opt into invoice data entry").option("--hide-country-field <on|off>", "hide the country field on the form").option(
17518
+ "--fixed-country <ISO|none>",
17519
+ "country stored on orders while the country field is hidden (none = Poland)"
17520
+ ).option(
17521
+ "--show-fixed-country <on|off>",
17522
+ "with the country field hidden: keep it on the form as a locked, read-only field"
17523
+ ).option("--show-phone-field <off|optional|required>", "phone field visibility").option("--show-additional-info <off|optional|required>", "additional-info field visibility").option("--collect-first-name <on|off>", "free checkouts: collect first name").option("--collect-last-name <on|off>", "free checkouts: collect last name").option(
17302
17524
  "--allow-promotion-code <off|subscriptions|all>",
17303
17525
  "promotion-code input: hidden, subscription prices only, or all prices"
17304
17526
  ).option("--force-b2b <on|off>", "force company (B2B) purchase data").option(
@@ -17454,7 +17676,10 @@ Order bumps (${checkout.orderBumps.length}):
17454
17676
  price.command("add").description("Add a price variant to an offer").argument("<checkoutId>", "offer (checkout) id").requiredOption("--type <type>", "one_time | recurring | installments").requiredOption(
17455
17677
  "--amount <minor-units>",
17456
17678
  "unit price in minor units, e.g. 1999 = 19.99 (installments: each payment)"
17457
- ).requiredOption("--currency <code>", "pln | usd | eur | gbp").option("--interval <interval>", "billing interval: day | week | month | year").option("--interval-count <n>", "bill every n intervals").option("--cycles <n>", "installments: total number of payments (>= 2)").option("--allow-cancellation", "installments: let the buyer cancel the schedule").option("--trial-days <n>", "recurring: free-trial days before the first charge").option("--label <text>", "display label").option("--invoice-name <text>", "custom invoice line name").option("--hidden", "hide from the public price list (buyable via direct link)").option("--description <text>", "markdown description rendered under the price").option("--sale-ends-at <ISO>", "time-limited sale deadline (ISO 8601, in the future)").option("--sale-days <n>", "convenience: end the sale n days from now").option("--quantity-limit <n>", "seat limit").option("--show-available", "show the remaining quantity publicly").option("--compare-at <minor-units>", 'promo strike-through "was" price in minor units').option("--access-interval <interval>", "one-time: limited access window unit").option("--access-count <n>", "one-time: limited access window length").option("--plan <planId>", "link the price to a plan").action(async (checkoutId, options, command) => {
17679
+ ).requiredOption("--currency <code>", "pln | usd | eur | gbp").option("--interval <interval>", "billing interval: day | week | month | year").option("--interval-count <n>", "bill every n intervals").option("--cycles <n>", "installments: total number of payments (>= 2)").option("--allow-cancellation", "installments: let the buyer cancel the schedule").option("--trial-days <n>", "recurring: free-trial days before the first charge").option("--label <text>", "display label").option("--invoice-name <text>", "custom invoice line name").option("--hidden", "hide from the public price list (buyable via direct link)").option("--description <text>", "markdown description rendered under the price").option("--sale-ends-at <ISO>", "time-limited sale deadline (ISO 8601, in the future)").option("--sale-days <n>", "convenience: end the sale n days from now").option("--quantity-limit <n>", "seat limit").option("--show-available", "show the remaining quantity publicly").option("--compare-at <minor-units>", 'promo strike-through "was" price in minor units').option("--access-interval <interval>", "one-time: limited access window unit").option("--access-count <n>", "one-time: limited access window length").option(
17680
+ "--access-until <iso>",
17681
+ "one-time: fixed access end for every buyer (ISO 8601, future; instead of --access-interval)"
17682
+ ).option("--plan <planId>", "link the price to a plan").action(async (checkoutId, options, command) => {
17458
17683
  const flags = getGlobalFlags(command);
17459
17684
  const input = buildPriceInput(options);
17460
17685
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
@@ -19932,7 +20157,7 @@ function formatSteps(steps) {
19932
20157
  // src/index.ts
19933
20158
  var PROGRAM_NAME = "zanfia".length > 0 ? "zanfia" : "zanfia";
19934
20159
  var program2 = new Command();
19935
- program2.name(PROGRAM_NAME).description("Zanfia CLI \u2014 manage products, clients, orders and more from your terminal").version("0.1.6").option("--api-key <key>", "override stored API key for this invocation").option("--json", "emit JSON output instead of human-readable tables").option("--verbose", "log request/response debug info").showHelpAfterError().addHelpText(
20160
+ program2.name(PROGRAM_NAME).description("Zanfia CLI \u2014 manage products, clients, orders and more from your terminal").version("0.3.10").option("--api-key <key>", "override stored API key for this invocation").option("--json", "emit JSON output instead of human-readable tables").option("--verbose", "log request/response debug info").showHelpAfterError().addHelpText(
19936
20161
  "after",
19937
20162
  `
19938
20163
  New to Zanfia?
@@ -19950,10 +20175,14 @@ Signing in \u2014 two roles, two consent pages:
19950
20175
  ${PROGRAM_NAME} auth set-key --api-url <api-community-url> <key>
19951
20176
  Member alternative without a browser: paste the key generated on the
19952
20177
  creator's platform under Settings \u2192 Developer.
20178
+ ${PROGRAM_NAME} join <handle>
20179
+ Join a public community from the terminal: emailed 6-digit code, then a
20180
+ member key is saved \u2014 no account or browser needed. Try: join crew-of-one
19953
20181
  `
19954
20182
  );
19955
20183
  registerLoginCommand(program2);
19956
20184
  registerRegisterCommand(program2);
20185
+ registerJoinCommand(program2);
19957
20186
  registerLogoutCommand(program2, clearCredentials);
19958
20187
  registerAuthCommands(program2);
19959
20188
  registerProductsCommands(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zanfia/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.10",
4
4
  "private": false,
5
5
  "description": "Zanfia CLI — thin HTTP client over the Zanfia Public API",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "tus-js-client": "^4.3.1"
29
29
  },
30
30
  "devDependencies": {
31
- "@mailingr/typescript-config": "^1.1.0",
31
+ "@mailingr/typescript-config": "^1.1.1",
32
32
  "@types/node": "^22.19.17",
33
33
  "tsup": "^8.3.0",
34
34
  "tsx": "^4.23.1",