zuora-cli 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/dist/index.js +690 -0
  4. package/package.json +40 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Li, Jianfeng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # Zuora CLI
2
+
3
+ Command-line interface for Zuora.
4
+
5
+ `zuora-cli` discovers the capabilities visible to your configured credentials and exposes them as domain-oriented commands.
6
+
7
+ Status: alpha, version `0.0.1-alpha.0`. Command coverage and generated command names can change while the CLI is still in alpha.
8
+
9
+ ## Requirements
10
+
11
+ Node.js `18` or newer.
12
+
13
+ ## Install
14
+
15
+ For the current alpha, install from this repository:
16
+
17
+ ```bash
18
+ npm install
19
+ npm run build
20
+ npm link
21
+ ```
22
+
23
+ This exposes the `zuora-cli` command on your PATH.
24
+
25
+ ## Configure
26
+
27
+ Set the Zuora base URL and choose one authentication method.
28
+
29
+ Client credentials:
30
+
31
+ ```bash
32
+ export ZUORA_BASE_URL="https://rest.apisandbox.zuora.com"
33
+ export ZUORA_CLIENT_ID="..."
34
+ export ZUORA_CLIENT_SECRET="..."
35
+ ```
36
+
37
+ Bearer token:
38
+
39
+ ```bash
40
+ export ZUORA_BASE_URL="https://rest.apisandbox.zuora.com"
41
+ export ZUORA_ACCESS_TOKEN="..."
42
+ ```
43
+
44
+ The `Bearer ` prefix is optional. You can also pass a token as a global option:
45
+
46
+ ```bash
47
+ zuora-cli --access-token "$ZUORA_ACCESS_TOKEN" list
48
+ ```
49
+
50
+ Optional environment variables:
51
+
52
+ | Variable | Purpose |
53
+ | --- | --- |
54
+ | `ZUORA_BEARER_TOKEN` | Alias for `ZUORA_ACCESS_TOKEN`. |
55
+ | `ZUORA_ENTITY_IDS` | Sets the Zuora entity scope header. |
56
+ | `ZUORA_ORG_IDS` | Sets the Zuora org scope header. |
57
+ | `ZUORA_VERSION` | Sets the Zuora API version header. |
58
+ | `ZUORA_CLI_TIMEOUT_MS` | Request timeout in milliseconds. Default: `120000`. |
59
+ | `ZUORA_CLI_SCHEMA_CACHE_TTL_MS` | Command metadata cache TTL in milliseconds. Default: `86400000`. |
60
+ | `ZUORA_CLI_CACHE_DIR` | Cache directory. Default: `~/.zuora-cli/cache`. |
61
+ | `ZUORA_CLI_DISABLE_TOKEN_CACHE` | Set to `true` to disable token cache reads and writes. |
62
+
63
+ Run `zuora-cli --help` to see the same environment variable list in the CLI.
64
+
65
+ ## Quick Start
66
+
67
+ List commands visible to your credentials:
68
+
69
+ ```bash
70
+ zuora-cli list
71
+ ```
72
+
73
+ Print command metadata as JSON:
74
+
75
+ ```bash
76
+ zuora-cli list --json
77
+ ```
78
+
79
+ Refresh command metadata:
80
+
81
+ ```bash
82
+ zuora-cli refresh
83
+ ```
84
+
85
+ Clear cached credentials and command metadata:
86
+
87
+ ```bash
88
+ zuora-cli cache clear
89
+ ```
90
+
91
+ Run a generated command:
92
+
93
+ ```bash
94
+ zuora-cli product create \
95
+ --name aaa \
96
+ --effective-start-date 2026-01-01 \
97
+ --effective-end-date 2099-12-31
98
+ ```
99
+
100
+ Query an object:
101
+
102
+ ```bash
103
+ zuora-cli object query --object-type account --page-size 1
104
+ ```
105
+
106
+ ## Command Shape
107
+
108
+ Generated commands follow an AWS-style shape:
109
+
110
+ ```text
111
+ zuora-cli <domain> <operation> [options]
112
+ ```
113
+
114
+ Examples:
115
+
116
+ | Command | Purpose |
117
+ | --- | --- |
118
+ | `zuora-cli account summary ...` | Retrieve account summary data. |
119
+ | `zuora-cli object query --object-type account` | Query Zuora object records. |
120
+ | `zuora-cli product create ...` | Create a product. |
121
+ | `zuora-cli commerce-product create ...` | Create a commerce product. |
122
+
123
+ The naming layer is dynamic:
124
+
125
+ * Repeated entity names are removed from operations. For example, commerce product creation appears as `commerce-product create`, not `commerce-product create-product`.
126
+ * Multi-operation capabilities are split into subcommands. For example, order management operations become commands such as `order get` and `order activate`.
127
+
128
+ ## Input Flags
129
+
130
+ Primitive fields use normal options:
131
+
132
+ ```bash
133
+ zuora-cli object query --object-type account --page-size 1
134
+ ```
135
+
136
+ Object and array fields use JSON options:
137
+
138
+ ```bash
139
+ zuora-cli some-domain some-operation --payload-json '{"name":"example"}'
140
+ ```
141
+
142
+ Use command help to inspect available options:
143
+
144
+ ```bash
145
+ zuora-cli <domain> <operation> --help
146
+ ```
147
+
148
+ ## Output
149
+
150
+ Command output defaults to the response returned by Zuora.
151
+
152
+ `zuora-cli list --json` is available for scripts that need command metadata. Generated business commands do not require a separate `--json` flag.
153
+
154
+ ## Caching
155
+
156
+ `zuora-cli` caches command metadata and generated access tokens locally so each command does not need to rediscover capabilities or request a new token.
157
+
158
+ The cache is stored under `~/.zuora-cli/cache` by default. Set `ZUORA_CLI_CACHE_DIR` to override it.
159
+
160
+ Access tokens are cached as local plaintext credential material. Cache directories and files are restricted to the current OS user where supported.
161
+
162
+ * Access tokens are reused until shortly before expiry.
163
+ * Changing the client secret creates a new token cache entry.
164
+ * Command metadata is cached for 24 hours by default.
165
+ * Run `zuora-cli refresh` to force command metadata refresh.
166
+ * Run `zuora-cli cache clear` to remove cached tokens and command metadata.
167
+ * Set `ZUORA_CLI_DISABLE_TOKEN_CACHE=true` to disable token cache reads and writes. Command metadata caching remains enabled.
168
+
169
+ ## Exit Codes
170
+
171
+ Commands exit with a nonzero status for parser errors, transport errors, and business validation failures returned by Zuora.
172
+
173
+ ## Troubleshooting
174
+
175
+ If a command is missing, run:
176
+
177
+ ```bash
178
+ zuora-cli refresh
179
+ zuora-cli list
180
+ ```
181
+
182
+ If authentication or command discovery behaves unexpectedly, clear local cache:
183
+
184
+ ```bash
185
+ zuora-cli cache clear
186
+ ```
187
+
188
+ If you are using a bearer token and also have client credential variables set, the explicit bearer token takes precedence.
package/dist/index.js ADDED
@@ -0,0 +1,690 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command2 } from "commander";
5
+
6
+ // src/cache.ts
7
+ import { createHash } from "crypto";
8
+ import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
9
+ import { homedir } from "os";
10
+ import { dirname, join } from "path";
11
+ function cacheRoot() {
12
+ return process.env.ZUORA_CLI_CACHE_DIR || join(homedir(), ".zuora-cli", "cache");
13
+ }
14
+ function digest(value) {
15
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 24);
16
+ }
17
+ function secretFingerprint(secret) {
18
+ return secret ? digest({ clientSecret: secret }) : void 0;
19
+ }
20
+ function bearerFingerprint(accessToken) {
21
+ return accessToken ? digest({ accessToken }) : void 0;
22
+ }
23
+ function isTokenCacheDisabled() {
24
+ return process.env.ZUORA_CLI_DISABLE_TOKEN_CACHE?.toLowerCase() === "true";
25
+ }
26
+ function tokenCachePath(config) {
27
+ return join(cacheRoot(), "tokens", `${digest({
28
+ restBaseUrl: config.restBaseUrl,
29
+ clientId: config.clientId,
30
+ clientSecretFingerprint: secretFingerprint(config.clientSecret)
31
+ })}.json`);
32
+ }
33
+ function toolsCachePath(config) {
34
+ return join(cacheRoot(), "tools", `${digest({
35
+ mcpUrl: config.mcpUrl,
36
+ auth: config.accessToken ? `bearer:${bearerFingerprint(config.accessToken)}` : `client:${config.clientId}`,
37
+ entityIds: config.entityIds,
38
+ orgIds: config.orgIds,
39
+ zuoraVersion: config.zuoraVersion
40
+ })}.json`);
41
+ }
42
+ async function readJson(path) {
43
+ try {
44
+ return JSON.parse(await readFile(path, "utf8"));
45
+ } catch (error) {
46
+ if (error?.code === "ENOENT") return void 0;
47
+ return void 0;
48
+ }
49
+ }
50
+ async function writeJson(path, value) {
51
+ const dir = dirname(path);
52
+ await mkdir(dir, { recursive: true, mode: 448 });
53
+ await chmod(dir, 448);
54
+ await writeFile(path, `${JSON.stringify(value, null, 2)}
55
+ `, { mode: 384 });
56
+ await chmod(path, 384);
57
+ }
58
+ async function readCachedToken(config) {
59
+ if (isTokenCacheDisabled()) return void 0;
60
+ const cached = await readJson(tokenCachePath(config));
61
+ if (!cached || cached.expiresAt <= Date.now()) return void 0;
62
+ return cached.accessToken;
63
+ }
64
+ async function writeCachedToken(config, accessToken, expiresInSeconds) {
65
+ if (isTokenCacheDisabled()) return;
66
+ const safeExpiresInSeconds = Number.isFinite(expiresInSeconds) ? expiresInSeconds : 3600;
67
+ const skewSeconds = Math.min(300, Math.floor(safeExpiresInSeconds / 2));
68
+ const expiresAt = Date.now() + Math.max(1, safeExpiresInSeconds - skewSeconds) * 1e3;
69
+ await writeJson(tokenCachePath(config), { accessToken, expiresAt });
70
+ }
71
+ async function readCachedTools(config) {
72
+ const cached = await readJson(toolsCachePath(config));
73
+ if (!cached || cached.expiresAt <= Date.now()) return void 0;
74
+ return cached.tools;
75
+ }
76
+ async function writeCachedTools(config, tools) {
77
+ await writeJson(toolsCachePath(config), {
78
+ tools,
79
+ expiresAt: Date.now() + config.schemaCacheTtlMs
80
+ });
81
+ }
82
+ async function clearCache() {
83
+ const root = cacheRoot();
84
+ await Promise.all([
85
+ rm(join(root, "tokens"), { recursive: true, force: true }),
86
+ rm(join(root, "tools"), { recursive: true, force: true })
87
+ ]);
88
+ }
89
+
90
+ // src/config.ts
91
+ function resolveZuoraUrls(rawUrl) {
92
+ const trimmed = rawUrl.trim().replace(/\/+$/, "");
93
+ if (!trimmed) {
94
+ throw new Error("ZUORA_BASE_URL is required.");
95
+ }
96
+ const parsed = new URL(trimmed);
97
+ const isMcpEndpoint = parsed.pathname.replace(/\/+$/, "") === "/mcp";
98
+ let restBaseUrl;
99
+ if (!isMcpEndpoint) {
100
+ restBaseUrl = trimmed;
101
+ } else if (parsed.hostname.startsWith("rest.") || parsed.hostname.startsWith("rest-")) {
102
+ restBaseUrl = `${parsed.protocol}//${parsed.host}`;
103
+ } else {
104
+ const stagingMatch = parsed.hostname.match(/^staging(\d+)\.zuora\.com$/);
105
+ const restHost = stagingMatch ? `rest-staging${stagingMatch[1]}.zuora.com` : `rest.${parsed.hostname}`;
106
+ restBaseUrl = `${parsed.protocol}//${restHost}`;
107
+ }
108
+ return {
109
+ restBaseUrl,
110
+ mcpUrl: `${restBaseUrl}/mcp`
111
+ };
112
+ }
113
+ function readOptionalEnv(name) {
114
+ const value = process.env[name]?.trim();
115
+ return value || void 0;
116
+ }
117
+ function normalizeAccessToken(rawToken) {
118
+ const token = rawToken?.trim();
119
+ if (!token) return void 0;
120
+ return token.replace(/^Bearer\s+/i, "").trim();
121
+ }
122
+ function loadConfig(overrides = {}) {
123
+ const baseUrl = process.env.ZUORA_BASE_URL || process.env.BASE_URL;
124
+ if (!baseUrl) {
125
+ throw new Error("ZUORA_BASE_URL is required.");
126
+ }
127
+ const accessToken = normalizeAccessToken(
128
+ overrides.accessToken || readOptionalEnv("ZUORA_ACCESS_TOKEN") || readOptionalEnv("ZUORA_BEARER_TOKEN")
129
+ );
130
+ const clientId = readOptionalEnv("ZUORA_CLIENT_ID");
131
+ const clientSecret = readOptionalEnv("ZUORA_CLIENT_SECRET");
132
+ if (!accessToken && (!clientId || !clientSecret)) {
133
+ throw new Error("Provide ZUORA_ACCESS_TOKEN or both ZUORA_CLIENT_ID and ZUORA_CLIENT_SECRET.");
134
+ }
135
+ const { restBaseUrl, mcpUrl } = resolveZuoraUrls(baseUrl);
136
+ const timeoutMs = Number(process.env.ZUORA_CLI_TIMEOUT_MS || process.env.REMOTE_MCP_TIMEOUT_MS || 12e4);
137
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
138
+ throw new Error("ZUORA_CLI_TIMEOUT_MS must be a positive number.");
139
+ }
140
+ const schemaCacheTtlMs = Number(process.env.ZUORA_CLI_SCHEMA_CACHE_TTL_MS || 24 * 60 * 60 * 1e3);
141
+ if (!Number.isFinite(schemaCacheTtlMs) || schemaCacheTtlMs <= 0) {
142
+ throw new Error("ZUORA_CLI_SCHEMA_CACHE_TTL_MS must be a positive number.");
143
+ }
144
+ return {
145
+ restBaseUrl,
146
+ mcpUrl,
147
+ clientId,
148
+ clientSecret,
149
+ accessToken,
150
+ timeoutMs,
151
+ schemaCacheTtlMs,
152
+ entityIds: readOptionalEnv("ZUORA_ENTITY_IDS"),
153
+ orgIds: readOptionalEnv("ZUORA_ORG_IDS"),
154
+ zuoraVersion: readOptionalEnv("ZUORA_VERSION")
155
+ };
156
+ }
157
+
158
+ // src/mcpClient.ts
159
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
160
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
161
+
162
+ // src/http.ts
163
+ function createTimedFetch(timeoutMs) {
164
+ return async (input, init = {}) => {
165
+ const controller = new AbortController();
166
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
167
+ const abortFromCaller = () => controller.abort();
168
+ init.signal?.addEventListener("abort", abortFromCaller, { once: true });
169
+ try {
170
+ return await fetch(input, {
171
+ ...init,
172
+ signal: controller.signal
173
+ });
174
+ } finally {
175
+ clearTimeout(timeout);
176
+ init.signal?.removeEventListener("abort", abortFromCaller);
177
+ }
178
+ };
179
+ }
180
+
181
+ // src/auth.ts
182
+ function oneIdTokenUrl(restBaseUrl) {
183
+ const host = new URL(restBaseUrl).hostname.toLowerCase();
184
+ const isStaging = host.includes("staging") || host.includes(".stg.") || host.includes("-stg");
185
+ return isStaging ? "https://one.stg.na.zuora.com/oauth2/token" : "https://one.zuora.com/oauth2/token";
186
+ }
187
+ async function requestToken(url, config) {
188
+ if (!config.clientId || !config.clientSecret) {
189
+ throw new Error("ZUORA_CLIENT_ID and ZUORA_CLIENT_SECRET are required to generate an access token.");
190
+ }
191
+ const body = new URLSearchParams({
192
+ grant_type: "client_credentials",
193
+ client_id: config.clientId,
194
+ client_secret: config.clientSecret
195
+ });
196
+ const response = await createTimedFetch(config.timeoutMs)(url, {
197
+ method: "POST",
198
+ headers: {
199
+ "Content-Type": "application/x-www-form-urlencoded"
200
+ },
201
+ body
202
+ });
203
+ if (!response.ok) {
204
+ const responseBody = await response.text().catch(() => "");
205
+ throw new Error(`${response.status} ${response.statusText}${responseBody ? `: ${responseBody}` : ""}`);
206
+ }
207
+ const payload = await response.json();
208
+ if (!payload.access_token) {
209
+ throw new Error("OAuth response did not include access_token.");
210
+ }
211
+ return {
212
+ accessToken: payload.access_token,
213
+ expiresIn: payload.expires_in || 3600
214
+ };
215
+ }
216
+ async function getAccessToken(config) {
217
+ if (config.accessToken) return config.accessToken;
218
+ const cachedToken = await readCachedToken(config);
219
+ if (cachedToken) return cachedToken;
220
+ const endpoints = [
221
+ `${config.restBaseUrl}/oauth/token`,
222
+ oneIdTokenUrl(config.restBaseUrl)
223
+ ];
224
+ let lastError;
225
+ for (const endpoint of endpoints) {
226
+ try {
227
+ const token = await requestToken(endpoint, config);
228
+ await writeCachedToken(config, token.accessToken, token.expiresIn);
229
+ return token.accessToken;
230
+ } catch (error) {
231
+ lastError = error;
232
+ }
233
+ }
234
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
235
+ throw new Error(`Authentication failed: ${message}`);
236
+ }
237
+
238
+ // src/version.ts
239
+ var VERSION = "0.0.1-alpha.0";
240
+
241
+ // src/mcpClient.ts
242
+ function requestHeaders(config, token) {
243
+ return {
244
+ Authorization: `Bearer ${token}`,
245
+ "User-Agent": `zuora-cli/${VERSION} (Node/${process.version})`,
246
+ ...config.entityIds ? { "Zuora-Entity-Ids": config.entityIds } : {},
247
+ ...config.orgIds ? { "Zuora-Org-Ids": config.orgIds } : {},
248
+ ...config.zuoraVersion ? { "Zuora-Version": config.zuoraVersion } : {}
249
+ };
250
+ }
251
+ async function withClient(config, callback) {
252
+ const token = await getAccessToken(config);
253
+ const transport = new StreamableHTTPClientTransport(new URL(config.mcpUrl), {
254
+ requestInit: {
255
+ headers: requestHeaders(config, token)
256
+ },
257
+ fetch: createTimedFetch(config.timeoutMs)
258
+ });
259
+ const client = new Client({ name: "zuora-cli", version: VERSION });
260
+ try {
261
+ await client.connect(transport, { timeout: config.timeoutMs });
262
+ return await callback(client);
263
+ } finally {
264
+ await client.close().catch(() => void 0);
265
+ }
266
+ }
267
+ async function fetchTools(config) {
268
+ return withClient(config, async (client) => {
269
+ const result = await client.listTools(void 0, { timeout: config.timeoutMs });
270
+ return result.tools;
271
+ });
272
+ }
273
+ async function listTools(config, options = {}) {
274
+ if (!options.refresh) {
275
+ const cachedTools = await readCachedTools(config);
276
+ if (cachedTools) return cachedTools;
277
+ }
278
+ const tools = await fetchTools(config);
279
+ await writeCachedTools(config, tools);
280
+ return tools;
281
+ }
282
+ async function callTool(config, name, args) {
283
+ return withClient(config, async (client) => {
284
+ return client.callTool(
285
+ { name, arguments: args },
286
+ void 0,
287
+ { timeout: config.timeoutMs }
288
+ );
289
+ });
290
+ }
291
+
292
+ // src/schemaCli.ts
293
+ import { Option } from "commander";
294
+ var LEADING_VERBS = /* @__PURE__ */ new Set([
295
+ "activate",
296
+ "add",
297
+ "apply",
298
+ "ask",
299
+ "cancel",
300
+ "close",
301
+ "collect",
302
+ "create",
303
+ "deactivate",
304
+ "delete",
305
+ "download",
306
+ "email",
307
+ "export",
308
+ "explain",
309
+ "generate",
310
+ "get",
311
+ "import",
312
+ "list",
313
+ "manage",
314
+ "match",
315
+ "pending",
316
+ "post",
317
+ "preview",
318
+ "query",
319
+ "read",
320
+ "regenerate",
321
+ "renew",
322
+ "reopen",
323
+ "reverse",
324
+ "run",
325
+ "search",
326
+ "skip",
327
+ "submit",
328
+ "sync",
329
+ "transfer",
330
+ "unapply",
331
+ "update",
332
+ "upload",
333
+ "validate",
334
+ "write"
335
+ ]);
336
+ function kebab(value) {
337
+ return value.replace(/([a-z\d])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").replace(/[^a-zA-Z\d-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
338
+ }
339
+ function tokens(value) {
340
+ return kebab(value).split("-").filter(Boolean);
341
+ }
342
+ function camelFromKebab(value) {
343
+ return value.replace(/-([a-z\d])/g, (_, char) => char.toUpperCase());
344
+ }
345
+ function singularToken(token) {
346
+ if (token.endsWith("ies") && token.length > 3) return `${token.slice(0, -3)}y`;
347
+ if (token.endsWith("s") && !token.endsWith("ss")) return token.slice(0, -1);
348
+ return token;
349
+ }
350
+ function singularPhrase(parts) {
351
+ if (!parts.length) return "tool";
352
+ return [...parts.slice(0, -1), singularToken(parts.at(-1))].join("-");
353
+ }
354
+ function oneLineDescription(description) {
355
+ if (!description) return "";
356
+ return description.replace(/\s+/g, " ").trim().slice(0, 180);
357
+ }
358
+ function schemaType(schema) {
359
+ if (Array.isArray(schema.type)) {
360
+ return schema.type.find((type) => type !== "null");
361
+ }
362
+ return schema.type;
363
+ }
364
+ function optionValueType(schema) {
365
+ const type = schemaType(schema);
366
+ if (type === "boolean") return "boolean";
367
+ if (type === "integer" || type === "number") return "number";
368
+ if (type === "object" || type === "array" || schema.properties || schema.items) return "json";
369
+ return "string";
370
+ }
371
+ function optionPlans(tool, injectedArgNames = /* @__PURE__ */ new Set()) {
372
+ const properties = tool.inputSchema?.properties ?? {};
373
+ const required = new Set(tool.inputSchema?.required ?? []);
374
+ const usedFlagNames = /* @__PURE__ */ new Set();
375
+ return Object.entries(properties).filter(([sourceName]) => !injectedArgNames.has(sourceName)).map(([sourceName, schema], index) => {
376
+ const valueType = optionValueType(schema);
377
+ const baseFlagName = kebab(sourceName);
378
+ let flagName = valueType === "json" && !baseFlagName.endsWith("-json") ? `${baseFlagName}-json` : baseFlagName;
379
+ if (usedFlagNames.has(flagName)) {
380
+ const fallbackFlagName = `${baseFlagName}-value`;
381
+ flagName = usedFlagNames.has(fallbackFlagName) ? `${baseFlagName}-${index + 1}` : fallbackFlagName;
382
+ }
383
+ usedFlagNames.add(flagName);
384
+ const flags = valueType === "boolean" ? `--${flagName}` : `--${flagName} <value>`;
385
+ const choices = schema.enum?.filter((value) => value !== null && value !== void 0).map(String);
386
+ return {
387
+ sourceName,
388
+ flagName,
389
+ commanderKey: camelFromKebab(flagName),
390
+ flags,
391
+ description: schema.description || (valueType === "json" ? `JSON value for ${sourceName}.` : sourceName),
392
+ required: required.has(sourceName),
393
+ valueType,
394
+ choices: choices?.length ? choices : void 0
395
+ };
396
+ });
397
+ }
398
+ function parseOptionValue(plan, value) {
399
+ if (plan.valueType === "boolean") return Boolean(value);
400
+ if (plan.valueType === "number") {
401
+ const numberValue = Number(value);
402
+ if (!Number.isFinite(numberValue)) {
403
+ throw new Error(`--${plan.flagName} must be a number.`);
404
+ }
405
+ return numberValue;
406
+ }
407
+ if (plan.valueType === "json") {
408
+ try {
409
+ return typeof value === "string" ? JSON.parse(value) : value;
410
+ } catch (error) {
411
+ const message = error instanceof Error ? error.message : String(error);
412
+ throw new Error(`--${plan.flagName} must be valid JSON: ${message}`);
413
+ }
414
+ }
415
+ return value;
416
+ }
417
+ function argumentsFromOptions(tool, options, injectedArgs = {}) {
418
+ const injectedArgNames = new Set(Object.keys(injectedArgs));
419
+ const args = { ...injectedArgs };
420
+ for (const plan of optionPlans(tool, injectedArgNames)) {
421
+ const value = options[plan.commanderKey];
422
+ if (value !== void 0) {
423
+ args[plan.sourceName] = parseOptionValue(plan, value);
424
+ }
425
+ }
426
+ return args;
427
+ }
428
+ function stripLeadingVerb(parts) {
429
+ const [first, ...rest] = parts;
430
+ return first && LEADING_VERBS.has(first) ? { verb: first, rest } : { rest: parts };
431
+ }
432
+ function routeFromToolName(toolName) {
433
+ const parsed = stripLeadingVerb(tokens(toolName));
434
+ const verb = parsed.verb || "run";
435
+ const rest = parsed.rest.length ? parsed.rest : tokens(toolName);
436
+ if (verb === "get" && rest.length > 1) {
437
+ return {
438
+ group: singularPhrase([rest[0]]),
439
+ operation: rest.slice(1).join("-"),
440
+ toolName,
441
+ description: ""
442
+ };
443
+ }
444
+ if (verb === "query") {
445
+ return {
446
+ group: singularPhrase(rest),
447
+ operation: "query",
448
+ toolName,
449
+ description: ""
450
+ };
451
+ }
452
+ return {
453
+ group: singularPhrase(rest),
454
+ operation: verb,
455
+ toolName,
456
+ description: ""
457
+ };
458
+ }
459
+ function removeMatchingEntityFromOperation(operationParts, entityParts) {
460
+ const singularEntity = entityParts.map(singularToken);
461
+ const normalizedOperation = operationParts.map(singularToken);
462
+ for (let i = 0; i <= normalizedOperation.length - singularEntity.length; i += 1) {
463
+ const matches = singularEntity.every((part, offset) => normalizedOperation[i + offset] === part);
464
+ if (matches) {
465
+ return [
466
+ ...operationParts.slice(0, i),
467
+ ...operationParts.slice(i + singularEntity.length)
468
+ ];
469
+ }
470
+ }
471
+ for (let length = Math.min(singularEntity.length, normalizedOperation.length - 1); length > 0; length -= 1) {
472
+ const entitySuffix = singularEntity.slice(-length);
473
+ for (let i = 1; i <= normalizedOperation.length - length; i += 1) {
474
+ const matches = entitySuffix.every((part, offset) => normalizedOperation[i + offset] === part);
475
+ if (matches) {
476
+ return [
477
+ ...operationParts.slice(0, i),
478
+ ...operationParts.slice(i + length)
479
+ ];
480
+ }
481
+ }
482
+ }
483
+ return operationParts;
484
+ }
485
+ function routeFromOperationEnum(tool, operationValue) {
486
+ const base = routeFromToolName(tool.name);
487
+ const groupParts = tokens(base.group);
488
+ const rawOperationParts = tokens(operationValue);
489
+ const cleanedParts = removeMatchingEntityFromOperation(rawOperationParts, groupParts);
490
+ const operation = cleanedParts.length ? cleanedParts.join("-") : base.operation;
491
+ return {
492
+ group: base.group,
493
+ operation,
494
+ toolName: tool.name,
495
+ description: oneLineDescription(tool.description),
496
+ injectedArgs: { operation: operationValue }
497
+ };
498
+ }
499
+ function operationEnumValues(tool) {
500
+ const operationSchema = tool.inputSchema?.properties?.operation;
501
+ if (!operationSchema?.enum?.length) return [];
502
+ return operationSchema.enum.filter((value) => typeof value === "string" && value.length > 0);
503
+ }
504
+ function routesForTool(tool) {
505
+ const enumValues = operationEnumValues(tool);
506
+ if (enumValues.length) {
507
+ return enumValues.map((operationValue) => routeFromOperationEnum(tool, operationValue));
508
+ }
509
+ const route = routeFromToolName(tool.name);
510
+ return [{
511
+ ...route,
512
+ description: oneLineDescription(tool.description)
513
+ }];
514
+ }
515
+ function routeRows(tools) {
516
+ return tools.flatMap(routesForTool).map((route) => ({
517
+ command: `${route.group} ${route.operation}`,
518
+ description: route.description
519
+ })).sort((a, b) => a.command.localeCompare(b.command));
520
+ }
521
+ function commandDescription(route) {
522
+ return route.description || `Run ${route.group} ${route.operation}`;
523
+ }
524
+ function registerToolCommands(program, tools, runner) {
525
+ const groups = /* @__PURE__ */ new Map();
526
+ const registeredPaths = /* @__PURE__ */ new Set();
527
+ for (const tool of tools) {
528
+ for (const route of routesForTool(tool)) {
529
+ let groupCommand = groups.get(route.group);
530
+ if (!groupCommand) {
531
+ groupCommand = program.command(route.group).description(`${route.group} commands`);
532
+ groups.set(route.group, groupCommand);
533
+ }
534
+ const path = `${route.group} ${route.operation}`;
535
+ const operationName = registeredPaths.has(path) ? `${route.operation}-${kebab(route.toolName)}` : route.operation;
536
+ registeredPaths.add(`${route.group} ${operationName}`);
537
+ const injectedArgs = route.injectedArgs ?? {};
538
+ const injectedArgNames = new Set(Object.keys(injectedArgs));
539
+ const command = groupCommand.command(operationName).description(commandDescription(route));
540
+ for (const plan of optionPlans(tool, injectedArgNames)) {
541
+ const option = new Option(plan.flags, plan.description);
542
+ if (plan.required) option.makeOptionMandatory();
543
+ if (plan.choices) option.choices(plan.choices);
544
+ command.addOption(option);
545
+ }
546
+ command.action(async (options) => {
547
+ const args = argumentsFromOptions(tool, options, injectedArgs);
548
+ await runner(tool.name, args);
549
+ });
550
+ }
551
+ }
552
+ }
553
+ function shouldLoadDynamicCommands(argv, staticCommands) {
554
+ const command = argv.find((arg) => !arg.startsWith("-"));
555
+ return !!command && command !== "help" && !staticCommands.has(command);
556
+ }
557
+
558
+ // src/output.ts
559
+ function isObject(value) {
560
+ return !!value && typeof value === "object" && !Array.isArray(value);
561
+ }
562
+ function textContent(result) {
563
+ const content = result.content;
564
+ if (!Array.isArray(content) || content.length === 0) return void 0;
565
+ const textParts = content.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text);
566
+ return textParts.length ? textParts.join("\n") : void 0;
567
+ }
568
+ function parseJsonText(text) {
569
+ try {
570
+ return JSON.parse(text);
571
+ } catch {
572
+ return void 0;
573
+ }
574
+ }
575
+ function isFailurePayload(value) {
576
+ if (!isObject(value)) return false;
577
+ if (value.success === false) return true;
578
+ if (value.error === true) return true;
579
+ if (value.type === "http_error" || value.type === "network_error") return true;
580
+ if (typeof value.status === "number" && value.status >= 400) return true;
581
+ const nested = [value.data, value.result, value.response, value.body];
582
+ return nested.some((nestedValue) => isFailurePayload(nestedValue));
583
+ }
584
+ function getToolResultExitCode(result) {
585
+ if (result.isError) return 1;
586
+ if (isFailurePayload(result.structuredContent)) return 1;
587
+ const text = textContent(result);
588
+ const parsedText = text ? parseJsonText(text) : void 0;
589
+ if (isFailurePayload(parsedText)) return 1;
590
+ return 0;
591
+ }
592
+ function printToolResult(result) {
593
+ const text = textContent(result);
594
+ if (text) {
595
+ console.log(text);
596
+ return getToolResultExitCode(result);
597
+ }
598
+ console.log(JSON.stringify(result, null, 2));
599
+ return getToolResultExitCode(result);
600
+ }
601
+ function printRows(rows) {
602
+ console.table(rows);
603
+ }
604
+ function printRowsJson(rows) {
605
+ console.log(JSON.stringify(rows, null, 2));
606
+ }
607
+
608
+ // src/index.ts
609
+ var STATIC_COMMANDS = /* @__PURE__ */ new Set(["list", "refresh", "cache"]);
610
+ var ENVIRONMENT_HELP = `
611
+ Environment variables:
612
+ ZUORA_BASE_URL Zuora REST base URL, for example https://rest.apisandbox.zuora.com
613
+ ZUORA_CLIENT_ID Client credentials auth: OAuth client id
614
+ ZUORA_CLIENT_SECRET Client credentials auth: OAuth client secret
615
+ ZUORA_ACCESS_TOKEN Bearer token auth; also accepted as --access-token
616
+ ZUORA_BEARER_TOKEN Alias for ZUORA_ACCESS_TOKEN
617
+ ZUORA_ENTITY_IDS Optional Zuora entity scope header
618
+ ZUORA_ORG_IDS Optional Zuora org scope header
619
+ ZUORA_VERSION Optional Zuora API version header
620
+ ZUORA_CLI_TIMEOUT_MS Request timeout in milliseconds
621
+ ZUORA_CLI_SCHEMA_CACHE_TTL_MS Command metadata cache TTL in milliseconds
622
+ ZUORA_CLI_CACHE_DIR Cache directory; defaults to ~/.zuora-cli/cache
623
+ ZUORA_CLI_DISABLE_TOKEN_CACHE Set to true to disable token cache reads and writes
624
+
625
+ Authentication requires either ZUORA_ACCESS_TOKEN/ZUORA_BEARER_TOKEN or both
626
+ ZUORA_CLIENT_ID and ZUORA_CLIENT_SECRET.`;
627
+ function readAccessTokenOption(argv) {
628
+ for (let i = 0; i < argv.length; i += 1) {
629
+ const arg = argv[i];
630
+ if (arg === "--access-token") {
631
+ return argv[i + 1];
632
+ }
633
+ if (arg.startsWith("--access-token=")) {
634
+ return arg.slice("--access-token=".length);
635
+ }
636
+ }
637
+ return void 0;
638
+ }
639
+ function createProgram(argv) {
640
+ const program = new Command2();
641
+ program.name("zuora-cli").description("Command-line interface for Zuora").version(VERSION).option("--access-token <token>", "Bearer access token; can also use ZUORA_ACCESS_TOKEN").showHelpAfterError().addHelpText("after", `${ENVIRONMENT_HELP}
642
+
643
+ Commands are generated from the capabilities visible to your configured credentials.
644
+ Use "zuora-cli list" to list available commands.`);
645
+ program.command("list").description("List available commands").option("--json", "Print available commands as JSON").action(async (options) => {
646
+ const config = loadConfig({ accessToken: readAccessTokenOption(argv) });
647
+ const tools = await listTools(config);
648
+ const rows = routeRows(tools);
649
+ if (options.json) {
650
+ printRowsJson(rows);
651
+ } else {
652
+ printRows(rows);
653
+ }
654
+ });
655
+ program.command("refresh").description("Refresh available command metadata").action(async () => {
656
+ const config = loadConfig({ accessToken: readAccessTokenOption(argv) });
657
+ const tools = await listTools(config, { refresh: true });
658
+ console.log(`Refreshed ${routeRows(tools).length} commands.`);
659
+ });
660
+ const cache = program.command("cache").description("Manage local cache");
661
+ cache.command("clear").description("Clear cached credentials and command metadata").action(async () => {
662
+ await clearCache();
663
+ console.log("Cache cleared.");
664
+ });
665
+ return program;
666
+ }
667
+ async function main(argv = process.argv) {
668
+ const program = createProgram(argv);
669
+ if (shouldLoadDynamicCommands(argv.slice(2), STATIC_COMMANDS)) {
670
+ const config = loadConfig({ accessToken: readAccessTokenOption(argv) });
671
+ const tools = await listTools(config);
672
+ registerToolCommands(program, tools, async (toolName, args) => {
673
+ const result = await callTool(config, toolName, args);
674
+ const exitCode = printToolResult(result);
675
+ if (exitCode !== 0) {
676
+ process.exitCode = exitCode;
677
+ }
678
+ });
679
+ }
680
+ await program.parseAsync(argv);
681
+ }
682
+ main().catch((error) => {
683
+ const message = error instanceof Error ? error.message : String(error);
684
+ console.error(`Error: ${message}`);
685
+ process.exit(1);
686
+ });
687
+ export {
688
+ main
689
+ };
690
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "zuora-cli",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "Command-line interface for Zuora",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "zuora-cli": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/index.js",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "postbuild": "chmod +x dist/index.js",
18
+ "check": "tsc --noEmit",
19
+ "test": "vitest run",
20
+ "validate": "npm run check && npm test && npm run build"
21
+ },
22
+ "keywords": [
23
+ "zuora",
24
+ "cli"
25
+ ],
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.24.0",
32
+ "commander": "^14.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.13.10",
36
+ "tsup": "^8.4.0",
37
+ "typescript": "^5.8.3",
38
+ "vitest": "^3.2.4"
39
+ }
40
+ }