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.
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/index.js +690 -0
- 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
|
+
}
|