@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.
- package/README.md +52 -52
- package/dist/index.js +548 -144
- 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
|
|
6911
|
-
var
|
|
6910
|
+
var CREDENTIALS_STORE_VERSION = 2;
|
|
6911
|
+
var MEMBER_PROFILE_PREFIX = "member:";
|
|
6912
|
+
var FALLBACK_PROFILE_KEY = "default";
|
|
6913
|
+
function configDir() {
|
|
6914
|
+
return process.env.ZANFIA_CONFIG_DIR || join(homedir(), ".config", "zanfia");
|
|
6915
|
+
}
|
|
6916
|
+
function credentialsFilePath() {
|
|
6917
|
+
return join(configDir(), "credentials.json");
|
|
6918
|
+
}
|
|
6912
6919
|
var orUndefined = (value) => value.length > 0 ? value : void 0;
|
|
6913
6920
|
var backendDefaults = {
|
|
6914
6921
|
apiUrl: true ? orUndefined("https://api.zanfia.com") : void 0,
|
|
@@ -6929,24 +6936,110 @@ function resolvePlatformUrl() {
|
|
|
6929
6936
|
const raw = process.env.ZANFIA_PLATFORM_URL || backendDefaults.platformUrl || "https://zanfia.co";
|
|
6930
6937
|
return raw.replace(/\/+$/, "");
|
|
6931
6938
|
}
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6939
|
+
var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6940
|
+
var isStoredCredentials = (value) => isRecord(value) && typeof value.apiKey === "string";
|
|
6941
|
+
var emptyStore = () => ({
|
|
6942
|
+
version: CREDENTIALS_STORE_VERSION,
|
|
6943
|
+
active: null,
|
|
6944
|
+
profiles: {}
|
|
6945
|
+
});
|
|
6946
|
+
function profileKeyFor(credentials) {
|
|
6947
|
+
const claims = decodeTokenClaims(credentials.apiKey);
|
|
6948
|
+
const kind = credentials.kind ?? claims.kind;
|
|
6949
|
+
const base = credentials.workspaceId ?? claims.workspaceId ?? FALLBACK_PROFILE_KEY;
|
|
6950
|
+
return kind === "member" ? `${MEMBER_PROFILE_PREFIX}${base}` : base;
|
|
6951
|
+
}
|
|
6952
|
+
function parseCredentialsStore(raw) {
|
|
6953
|
+
if (isStoredCredentials(raw)) {
|
|
6954
|
+
const key = profileKeyFor(raw);
|
|
6955
|
+
return { version: CREDENTIALS_STORE_VERSION, active: key, profiles: { [key]: raw } };
|
|
6956
|
+
}
|
|
6957
|
+
if (isRecord(raw) && isRecord(raw.profiles)) {
|
|
6958
|
+
const profiles = {};
|
|
6959
|
+
for (const [key, value] of Object.entries(raw.profiles)) {
|
|
6960
|
+
if (isStoredCredentials(value)) profiles[key] = value;
|
|
6938
6961
|
}
|
|
6939
|
-
|
|
6962
|
+
const keys = Object.keys(profiles);
|
|
6963
|
+
const active = typeof raw.active === "string" && raw.active in profiles ? raw.active : keys[0] ?? null;
|
|
6964
|
+
return { version: CREDENTIALS_STORE_VERSION, active, profiles };
|
|
6965
|
+
}
|
|
6966
|
+
return emptyStore();
|
|
6967
|
+
}
|
|
6968
|
+
function readCredentialsStore() {
|
|
6969
|
+
try {
|
|
6970
|
+
return parseCredentialsStore(JSON.parse(readFileSync(credentialsFilePath(), "utf8")));
|
|
6940
6971
|
} catch {
|
|
6941
|
-
return
|
|
6972
|
+
return emptyStore();
|
|
6942
6973
|
}
|
|
6943
6974
|
}
|
|
6975
|
+
function writeCredentialsStore(store) {
|
|
6976
|
+
const path2 = credentialsFilePath();
|
|
6977
|
+
mkdirSync(dirname(path2), { recursive: true, mode: 448 });
|
|
6978
|
+
writeFileSync(path2, JSON.stringify(store, null, 2), { mode: 384 });
|
|
6979
|
+
return path2;
|
|
6980
|
+
}
|
|
6981
|
+
function readCredentials() {
|
|
6982
|
+
const store = readCredentialsStore();
|
|
6983
|
+
return store.active ? store.profiles[store.active] ?? null : null;
|
|
6984
|
+
}
|
|
6944
6985
|
function writeCredentials(credentials) {
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6986
|
+
const store = readCredentialsStore();
|
|
6987
|
+
const key = profileKeyFor(credentials);
|
|
6988
|
+
store.profiles[key] = credentials;
|
|
6989
|
+
store.active = key;
|
|
6990
|
+
return writeCredentialsStore(store);
|
|
6991
|
+
}
|
|
6992
|
+
function listStoredProfiles() {
|
|
6993
|
+
const store = readCredentialsStore();
|
|
6994
|
+
return Object.entries(store.profiles).map(([key, credentials]) => ({ key, credentials, active: key === store.active })).sort((a, b) => {
|
|
6995
|
+
if (a.active !== b.active) return a.active ? -1 : 1;
|
|
6996
|
+
return (a.credentials.workspaceName ?? a.key).localeCompare(
|
|
6997
|
+
b.credentials.workspaceName ?? b.key
|
|
6998
|
+
);
|
|
6948
6999
|
});
|
|
6949
|
-
|
|
7000
|
+
}
|
|
7001
|
+
function findStoredProfile(store, ref) {
|
|
7002
|
+
const wanted = ref.trim().toLowerCase();
|
|
7003
|
+
if (!wanted) return null;
|
|
7004
|
+
const match = Object.entries(store.profiles).find(
|
|
7005
|
+
([key, credentials]) => [key, credentials.workspaceId, credentials.workspaceSlug, credentials.workspaceName].some(
|
|
7006
|
+
(candidate) => typeof candidate === "string" && candidate.toLowerCase() === wanted
|
|
7007
|
+
)
|
|
7008
|
+
);
|
|
7009
|
+
return match ? { key: match[0], credentials: match[1], active: match[0] === store.active } : null;
|
|
7010
|
+
}
|
|
7011
|
+
var StoredWorkspaceNotFoundError = class extends Error {
|
|
7012
|
+
constructor(ref, available) {
|
|
7013
|
+
super(
|
|
7014
|
+
`No stored login for workspace "${ref}".` + (available.length > 0 ? ` Stored: ${available.join(", ")}.` : "") + ` Log in to it with: zanfia login --workspace ${ref}`
|
|
7015
|
+
);
|
|
7016
|
+
this.name = "StoredWorkspaceNotFoundError";
|
|
7017
|
+
}
|
|
7018
|
+
};
|
|
7019
|
+
var profileLabel = (profile) => profile.credentials.workspaceSlug ?? profile.credentials.workspaceId ?? profile.key;
|
|
7020
|
+
function useStoredProfile(ref) {
|
|
7021
|
+
const store = readCredentialsStore();
|
|
7022
|
+
const profile = findStoredProfile(store, ref);
|
|
7023
|
+
if (!profile) {
|
|
7024
|
+
throw new StoredWorkspaceNotFoundError(
|
|
7025
|
+
ref,
|
|
7026
|
+
Object.entries(store.profiles).map(([key, credentials]) => profileLabel({ key, credentials }))
|
|
7027
|
+
);
|
|
7028
|
+
}
|
|
7029
|
+
store.active = profile.key;
|
|
7030
|
+
writeCredentialsStore(store);
|
|
7031
|
+
return { ...profile, active: true };
|
|
7032
|
+
}
|
|
7033
|
+
function apiKeyForStoredWorkspace(ref) {
|
|
7034
|
+
const store = readCredentialsStore();
|
|
7035
|
+
const profile = findStoredProfile(store, ref);
|
|
7036
|
+
if (!profile) {
|
|
7037
|
+
throw new StoredWorkspaceNotFoundError(
|
|
7038
|
+
ref,
|
|
7039
|
+
Object.entries(store.profiles).map(([key, credentials]) => profileLabel({ key, credentials }))
|
|
7040
|
+
);
|
|
7041
|
+
}
|
|
7042
|
+
return profile.credentials.apiKey;
|
|
6950
7043
|
}
|
|
6951
7044
|
function resolveApiKey(explicit) {
|
|
6952
7045
|
if (explicit) return explicit;
|
|
@@ -6995,15 +7088,19 @@ function isMockMode() {
|
|
|
6995
7088
|
}
|
|
6996
7089
|
return resolveApiUrl() === null;
|
|
6997
7090
|
}
|
|
6998
|
-
function
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
return
|
|
7091
|
+
function removeStoredLogin(options = {}) {
|
|
7092
|
+
const store = readCredentialsStore();
|
|
7093
|
+
const removed = store.active ? store.profiles[store.active] ?? null : null;
|
|
7094
|
+
if (options.all || Object.keys(store.profiles).length <= 1) {
|
|
7095
|
+
rmSync(credentialsFilePath(), { force: true });
|
|
7096
|
+
return { removed, nowActive: null };
|
|
7004
7097
|
}
|
|
7098
|
+
if (store.active) delete store.profiles[store.active];
|
|
7099
|
+
const [nextKey] = Object.entries(store.profiles).sort(([, a], [, b]) => b.savedAt.localeCompare(a.savedAt)).map(([key]) => key);
|
|
7100
|
+
store.active = nextKey ?? null;
|
|
7101
|
+
writeCredentialsStore(store);
|
|
7102
|
+
return { removed, nowActive: nextKey ? store.profiles[nextKey] ?? null : null };
|
|
7005
7103
|
}
|
|
7006
|
-
var credentialsPath = CREDENTIALS_FILE;
|
|
7007
7104
|
|
|
7008
7105
|
// src/utils/errors.ts
|
|
7009
7106
|
var ApiError = class extends Error {
|
|
@@ -7671,6 +7768,9 @@ function createMockClient() {
|
|
|
7671
7768
|
if (!result) throw new ApiError(404, `Product ${id} not found`);
|
|
7672
7769
|
return result;
|
|
7673
7770
|
},
|
|
7771
|
+
async deleteProduct() {
|
|
7772
|
+
throw new ApiError(501, "product delete is not available in mock mode");
|
|
7773
|
+
},
|
|
7674
7774
|
async setProductStatus() {
|
|
7675
7775
|
throw new ApiError(501, "product status changes are not available in mock mode");
|
|
7676
7776
|
},
|
|
@@ -8343,6 +8443,9 @@ function createMockClient() {
|
|
|
8343
8443
|
async cancelEmailCampaign() {
|
|
8344
8444
|
throw new ApiError(501, "email campaigns are not available in mock mode");
|
|
8345
8445
|
},
|
|
8446
|
+
async rescheduleEmailCampaign() {
|
|
8447
|
+
throw new ApiError(501, "email campaigns are not available in mock mode");
|
|
8448
|
+
},
|
|
8346
8449
|
async sendEmailCampaignTest() {
|
|
8347
8450
|
throw new ApiError(501, "email campaigns are not available in mock mode");
|
|
8348
8451
|
},
|
|
@@ -8514,6 +8617,9 @@ function createMockClient() {
|
|
|
8514
8617
|
async removeDownloadFile() {
|
|
8515
8618
|
throw new ApiError(501, "downloads content is not available in mock mode");
|
|
8516
8619
|
},
|
|
8620
|
+
async renameDownloadFile() {
|
|
8621
|
+
throw new ApiError(501, "downloads content is not available in mock mode");
|
|
8622
|
+
},
|
|
8517
8623
|
async reorderDownloadFiles() {
|
|
8518
8624
|
throw new ApiError(501, "downloads content is not available in mock mode");
|
|
8519
8625
|
},
|
|
@@ -9045,6 +9151,7 @@ function createHttpClient(config) {
|
|
|
9045
9151
|
createProduct: (input) => request("POST", "/products", input),
|
|
9046
9152
|
updateProduct: (id, input) => request("PATCH", `/products/${encodeURIComponent(id)}`, input),
|
|
9047
9153
|
archiveProduct: (id) => request("POST", `/products/${encodeURIComponent(id)}/archive`),
|
|
9154
|
+
deleteProduct: (id) => request("DELETE", `/products/${encodeURIComponent(id)}`),
|
|
9048
9155
|
setProductStatus: (id, status) => request("POST", `/products/${encodeURIComponent(id)}/status`, { status }),
|
|
9049
9156
|
listProductPrices: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/prices`),
|
|
9050
9157
|
createProductPrice: (productId, input) => request("POST", `/products/${encodeURIComponent(productId)}/prices`, input),
|
|
@@ -9123,6 +9230,8 @@ function createHttpClient(config) {
|
|
|
9123
9230
|
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
9124
9231
|
if (options?.cursor) params.set("cursor", options.cursor);
|
|
9125
9232
|
if (options?.products) params.set("products", options.products);
|
|
9233
|
+
if (options?.createdFrom) params.set("createdFrom", options.createdFrom);
|
|
9234
|
+
if (options?.createdTo) params.set("createdTo", options.createdTo);
|
|
9126
9235
|
const qs = params.toString();
|
|
9127
9236
|
return request("GET", `/clients${qs ? `?${qs}` : ""}`);
|
|
9128
9237
|
},
|
|
@@ -9538,6 +9647,7 @@ function createHttpClient(config) {
|
|
|
9538
9647
|
scheduledAt !== void 0 ? { scheduledAt } : {}
|
|
9539
9648
|
),
|
|
9540
9649
|
cancelEmailCampaign: (id) => request("POST", `/campaigns/${encodeURIComponent(id)}/cancel`),
|
|
9650
|
+
rescheduleEmailCampaign: (id, scheduledAt) => request("POST", `/campaigns/${encodeURIComponent(id)}/reschedule`, { scheduledAt }),
|
|
9541
9651
|
sendEmailCampaignTest: (id, recipients) => request(
|
|
9542
9652
|
"POST",
|
|
9543
9653
|
`/campaigns/${encodeURIComponent(id)}/test-sends`,
|
|
@@ -9669,6 +9779,11 @@ function createHttpClient(config) {
|
|
|
9669
9779
|
"DELETE",
|
|
9670
9780
|
`/products/${encodeURIComponent(productId)}/downloads/files/${encodeURIComponent(uid)}`
|
|
9671
9781
|
),
|
|
9782
|
+
renameDownloadFile: (productId, uid, name) => request(
|
|
9783
|
+
"PATCH",
|
|
9784
|
+
`/products/${encodeURIComponent(productId)}/downloads/files/${encodeURIComponent(uid)}`,
|
|
9785
|
+
{ name }
|
|
9786
|
+
),
|
|
9672
9787
|
reorderDownloadFiles: (productId, uids) => request("POST", `/products/${encodeURIComponent(productId)}/downloads/files/reorder`, {
|
|
9673
9788
|
uids
|
|
9674
9789
|
}),
|
|
@@ -10026,6 +10141,22 @@ function parseWorkflowKind(value) {
|
|
|
10026
10141
|
}
|
|
10027
10142
|
return kind;
|
|
10028
10143
|
}
|
|
10144
|
+
var STOREFRONT_VISIBILITIES = [
|
|
10145
|
+
"public",
|
|
10146
|
+
"unlisted",
|
|
10147
|
+
"private"
|
|
10148
|
+
];
|
|
10149
|
+
function parseStorefrontVisibility(value) {
|
|
10150
|
+
const visibility = STOREFRONT_VISIBILITIES.find(
|
|
10151
|
+
(candidate) => candidate === value.trim().toLowerCase()
|
|
10152
|
+
);
|
|
10153
|
+
if (visibility === void 0) {
|
|
10154
|
+
throw new Error(
|
|
10155
|
+
`Invalid --visibility: ${value} (expected one of: ${STOREFRONT_VISIBILITIES.join(", ")})`
|
|
10156
|
+
);
|
|
10157
|
+
}
|
|
10158
|
+
return visibility;
|
|
10159
|
+
}
|
|
10029
10160
|
function parseCommaList(flag) {
|
|
10030
10161
|
return (value) => {
|
|
10031
10162
|
const items = [
|
|
@@ -10065,6 +10196,8 @@ function requireConfirm(confirm, what) {
|
|
|
10065
10196
|
var OUTPUT_FORMAT_JSON = "json";
|
|
10066
10197
|
var API_KEY_FLAG = "--api-key <key>";
|
|
10067
10198
|
var API_KEY_DESCRIPTION = "override stored API key for this invocation";
|
|
10199
|
+
var WORKSPACE_FLAG = "--workspace <id|slug>";
|
|
10200
|
+
var WORKSPACE_DESCRIPTION = "use the stored login of this workspace for this invocation (see `workspace list`)";
|
|
10068
10201
|
var VERBOSE_FLAG = "--verbose";
|
|
10069
10202
|
var VERBOSE_DESCRIPTION = "log request/response debug info";
|
|
10070
10203
|
var JSON_FLAG = "--json";
|
|
@@ -10084,6 +10217,7 @@ function registerOnCommand(command) {
|
|
|
10084
10217
|
command.enablePositionalOptions();
|
|
10085
10218
|
}
|
|
10086
10219
|
addOptionIfMissing(command, new Option(API_KEY_FLAG, API_KEY_DESCRIPTION).hideHelp());
|
|
10220
|
+
addOptionIfMissing(command, new Option(WORKSPACE_FLAG, WORKSPACE_DESCRIPTION).hideHelp());
|
|
10087
10221
|
addOptionIfMissing(command, new Option(VERBOSE_FLAG, VERBOSE_DESCRIPTION).hideHelp());
|
|
10088
10222
|
addOptionIfMissing(
|
|
10089
10223
|
command,
|
|
@@ -10095,13 +10229,14 @@ function registerOnCommand(command) {
|
|
|
10095
10229
|
function applyGlobalOptions(program2) {
|
|
10096
10230
|
program2.enablePositionalOptions();
|
|
10097
10231
|
program2.option(API_KEY_FLAG, API_KEY_DESCRIPTION);
|
|
10232
|
+
program2.option(WORKSPACE_FLAG, WORKSPACE_DESCRIPTION);
|
|
10098
10233
|
program2.option(JSON_FLAG, JSON_DESCRIPTION);
|
|
10099
10234
|
program2.option(VERBOSE_FLAG, VERBOSE_DESCRIPTION);
|
|
10100
10235
|
for (const sub of program2.commands) registerOnCommand(sub);
|
|
10101
10236
|
}
|
|
10102
10237
|
|
|
10103
10238
|
// src/utils/output.ts
|
|
10104
|
-
function
|
|
10239
|
+
function isRecord2(value) {
|
|
10105
10240
|
return value !== null && typeof value === "object";
|
|
10106
10241
|
}
|
|
10107
10242
|
function getGlobalFlags(command) {
|
|
@@ -10112,11 +10247,15 @@ function getGlobalFlags(command) {
|
|
|
10112
10247
|
const flags = {};
|
|
10113
10248
|
for (const node of chain) {
|
|
10114
10249
|
const raw = node.opts();
|
|
10115
|
-
if (!
|
|
10250
|
+
if (!isRecord2(raw)) continue;
|
|
10116
10251
|
if (typeof raw.apiKey === "string") flags.apiKey = raw.apiKey;
|
|
10252
|
+
if (typeof raw.workspace === "string") flags.workspace = raw.workspace;
|
|
10117
10253
|
if (raw.verbose === true) flags.verbose = true;
|
|
10118
10254
|
if (raw.json === true || raw.output === OUTPUT_FORMAT_JSON) flags.json = true;
|
|
10119
10255
|
}
|
|
10256
|
+
if (flags.workspace && !flags.apiKey && !process.env.ZANFIA_API_KEY) {
|
|
10257
|
+
flags.apiKey = apiKeyForStoredWorkspace(flags.workspace);
|
|
10258
|
+
}
|
|
10120
10259
|
return flags;
|
|
10121
10260
|
}
|
|
10122
10261
|
function emit(flags, data, humanRenderer) {
|
|
@@ -10132,10 +10271,10 @@ function printJson(data) {
|
|
|
10132
10271
|
`);
|
|
10133
10272
|
}
|
|
10134
10273
|
function tableRowsOf(result, key) {
|
|
10135
|
-
if (!
|
|
10274
|
+
if (!isRecord2(result)) return [];
|
|
10136
10275
|
const rows = result[key];
|
|
10137
10276
|
if (!Array.isArray(rows)) return [];
|
|
10138
|
-
return rows.filter(
|
|
10277
|
+
return rows.filter(isRecord2);
|
|
10139
10278
|
}
|
|
10140
10279
|
function printTable(rows, columns) {
|
|
10141
10280
|
if (rows.length === 0) {
|
|
@@ -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
|
-
|
|
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(
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
11690
|
-
auth.command("logout").description(
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
run \`login
|
|
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(
|
|
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
|
|
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>", "
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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:
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
|
29886
|
-
return "0.4
|
|
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
|
|
30331
|
+
registerLogoutCommand(program2);
|
|
29928
30332
|
registerAuthCommands(program2);
|
|
29929
30333
|
registerProductsCommands(program2);
|
|
29930
30334
|
registerOffersCommands(program2);
|