@zapier/zapier-sdk-cli 0.30.0 → 0.31.1

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.
@@ -0,0 +1,234 @@
1
+ import { createWriteStream } from "fs";
2
+ import { promises as fs } from "fs";
3
+ import { dirname } from "path";
4
+ import { CurlSchema } from "./schemas";
5
+ import { CurlExitError, parseHeaderLine, basicAuthHeader, deriveRemoteFilename, formatWriteOut, appendQueryParams, readAllStdin, resolveDataArgText, resolveDataArgBinary, buildFormData, } from "./utils";
6
+ export const curlPlugin = ({ sdk, }) => {
7
+ async function curl(options) {
8
+ const { url: rawUrl, request, header = [], data = [], dataRaw = [], dataAscii = [], dataBinary = [], dataUrlencode = [], json, form = [], formString = [], get: forceGet, head: forceHead, location, include, output, remoteName, verbose, silent, showError, fail, failWithBody, writeOut, maxTime, user, compressed, connectionId, } = options;
9
+ const parsedUrl = new URL(rawUrl);
10
+ const headers = {};
11
+ for (const h of header) {
12
+ const parsed = parseHeaderLine(h);
13
+ if (parsed) {
14
+ headers[parsed.key] = parsed.value;
15
+ }
16
+ }
17
+ if (user) {
18
+ headers["Authorization"] = basicAuthHeader(user);
19
+ }
20
+ if (compressed) {
21
+ headers["Accept-Encoding"] = "gzip, deflate, br";
22
+ }
23
+ const rawTextDataArgs = [...data, ...dataRaw, ...dataAscii];
24
+ const rawBinaryDataArgs = [...dataBinary];
25
+ const rawUrlencodeArgs = [...dataUrlencode];
26
+ const hasForm = form.length > 0 || formString.length > 0;
27
+ const hasJson = json !== undefined;
28
+ const hasAnyData = hasJson ||
29
+ hasForm ||
30
+ rawTextDataArgs.length > 0 ||
31
+ rawBinaryDataArgs.length > 0 ||
32
+ rawUrlencodeArgs.length > 0;
33
+ let method = "GET";
34
+ if (forceHead) {
35
+ method = "HEAD";
36
+ }
37
+ else if (request) {
38
+ method = request;
39
+ }
40
+ else if (forceGet) {
41
+ method = "GET";
42
+ }
43
+ else if (hasAnyData) {
44
+ method = "POST";
45
+ }
46
+ let body;
47
+ let effectiveUrl = parsedUrl;
48
+ if (hasJson) {
49
+ if (!headers["Content-Type"]) {
50
+ headers["Content-Type"] = "application/json";
51
+ }
52
+ if (!headers["Accept"]) {
53
+ headers["Accept"] = "application/json";
54
+ }
55
+ body = json;
56
+ }
57
+ else if (hasForm) {
58
+ body = await buildFormData(form, formString);
59
+ }
60
+ else {
61
+ const resolvedTextParts = [];
62
+ for (const raw of rawTextDataArgs) {
63
+ resolvedTextParts.push(await resolveDataArgText(raw));
64
+ }
65
+ const resolvedUrlEncodeParts = [];
66
+ for (const raw of rawUrlencodeArgs) {
67
+ // @file => read file, URL-encode contents
68
+ if (raw.startsWith("@")) {
69
+ const content = await resolveDataArgText(raw);
70
+ resolvedUrlEncodeParts.push(encodeURIComponent(content));
71
+ continue;
72
+ }
73
+ const atIdx = raw.indexOf("@");
74
+ const eqIdx = raw.indexOf("=");
75
+ if (eqIdx !== -1) {
76
+ // name=value => encode both
77
+ const key = raw.slice(0, eqIdx);
78
+ const value = raw.slice(eqIdx + 1);
79
+ resolvedUrlEncodeParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
80
+ }
81
+ else if (atIdx !== -1) {
82
+ // name@file => read file, encode contents as value
83
+ const key = raw.slice(0, atIdx);
84
+ const filePath = raw.slice(atIdx + 1);
85
+ const buf = filePath === "-"
86
+ ? await readAllStdin()
87
+ : await fs.readFile(filePath);
88
+ resolvedUrlEncodeParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(buf.toString("utf8"))}`);
89
+ }
90
+ else {
91
+ resolvedUrlEncodeParts.push(encodeURIComponent(raw));
92
+ }
93
+ }
94
+ const resolvedBinaryParts = [];
95
+ for (const raw of rawBinaryDataArgs) {
96
+ resolvedBinaryParts.push(await resolveDataArgBinary(raw));
97
+ }
98
+ const allTextParts = [...resolvedTextParts, ...resolvedUrlEncodeParts];
99
+ if (forceGet && allTextParts.length > 0) {
100
+ effectiveUrl = appendQueryParams(parsedUrl, allTextParts);
101
+ }
102
+ else if (resolvedBinaryParts.length > 0) {
103
+ body = Buffer.concat(resolvedBinaryParts);
104
+ }
105
+ else if (allTextParts.length > 0) {
106
+ body = allTextParts.join("&");
107
+ if (!headers["Content-Type"]) {
108
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
109
+ }
110
+ }
111
+ }
112
+ // curl defaults to NOT following redirects
113
+ const redirect = location ? "follow" : "manual";
114
+ if (verbose && !silent) {
115
+ process.stderr.write(`> ${method} ${effectiveUrl.toString()}\n`);
116
+ for (const [k, v] of Object.entries(headers)) {
117
+ process.stderr.write(`> ${k}: ${v}\n`);
118
+ }
119
+ process.stderr.write(">\n");
120
+ }
121
+ const signal = maxTime ? AbortSignal.timeout(maxTime * 1000) : undefined;
122
+ const start = performance.now();
123
+ const response = await sdk.fetch(effectiveUrl.toString(), {
124
+ method,
125
+ headers,
126
+ body,
127
+ redirect,
128
+ signal,
129
+ connectionId,
130
+ });
131
+ const timeTotalSeconds = (performance.now() - start) / 1000;
132
+ if (verbose && !silent) {
133
+ process.stderr.write(`< HTTP ${response.status} ${response.statusText}\n`);
134
+ response.headers.forEach((value, key) => {
135
+ process.stderr.write(`< ${key}: ${value}\n`);
136
+ });
137
+ process.stderr.write("<\n");
138
+ }
139
+ const isHttpError = response.status >= 400;
140
+ const shouldFail = (fail || failWithBody) && isHttpError;
141
+ const shouldOutputBody = !shouldFail || !!failWithBody;
142
+ const headerText = include
143
+ ? `HTTP ${response.status} ${response.statusText}\n${Array.from(response.headers.entries())
144
+ .map(([k, v]) => `${k}: ${v}`)
145
+ .join("\n")}\n\n`
146
+ : "";
147
+ let bodyBytes = 0;
148
+ const buf = shouldOutputBody
149
+ ? Buffer.from(await response.arrayBuffer())
150
+ : Buffer.alloc(0);
151
+ bodyBytes = buf.length;
152
+ const outputFile = output && output !== "-"
153
+ ? output
154
+ : remoteName
155
+ ? deriveRemoteFilename(parsedUrl)
156
+ : undefined;
157
+ if (outputFile) {
158
+ const dir = dirname(outputFile);
159
+ if (dir !== ".") {
160
+ await fs.mkdir(dir, { recursive: true });
161
+ }
162
+ const ws = createWriteStream(outputFile);
163
+ if (headerText) {
164
+ ws.write(headerText);
165
+ }
166
+ if (buf.length) {
167
+ ws.write(buf);
168
+ }
169
+ await new Promise((resolve, reject) => {
170
+ ws.end(() => resolve());
171
+ ws.on("error", reject);
172
+ });
173
+ }
174
+ else {
175
+ if (headerText) {
176
+ process.stdout.write(headerText);
177
+ }
178
+ if (buf.length) {
179
+ process.stdout.write(buf);
180
+ }
181
+ }
182
+ if (writeOut) {
183
+ const formatted = formatWriteOut({
184
+ template: writeOut,
185
+ urlEffective: response.url || effectiveUrl.toString(),
186
+ httpCode: response.status,
187
+ timeTotalSeconds,
188
+ sizeDownloadBytes: bodyBytes,
189
+ contentType: response.headers.get("content-type"),
190
+ });
191
+ process.stdout.write(formatted);
192
+ }
193
+ if (shouldFail) {
194
+ if (!silent || showError) {
195
+ process.stderr.write(`curl: (22) The requested URL returned error: ${response.status}\n`);
196
+ }
197
+ throw new CurlExitError("HTTP request failed", 22);
198
+ }
199
+ return undefined;
200
+ }
201
+ return {
202
+ curl,
203
+ context: {
204
+ meta: {
205
+ curl: {
206
+ description: "Make authenticated HTTP requests to any API through Zapier. " +
207
+ "Pass a connection ID to automatically inject the user's stored credentials " +
208
+ "(OAuth tokens, API keys, etc.) into the outgoing request. " +
209
+ "Use it in place of the native curl command with additional Zapier-specific options.",
210
+ categories: ["http"],
211
+ inputSchema: CurlSchema,
212
+ aliases: {
213
+ request: "X",
214
+ header: "H",
215
+ data: "d",
216
+ form: "F",
217
+ get: "G",
218
+ head: "I",
219
+ location: "L",
220
+ include: "i",
221
+ output: "o",
222
+ remoteName: "O",
223
+ verbose: "v",
224
+ silent: "s",
225
+ showError: "S",
226
+ writeOut: "w",
227
+ maxTime: "m",
228
+ user: "u",
229
+ },
230
+ },
231
+ },
232
+ },
233
+ };
234
+ };
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ export declare const CurlSchema: z.ZodObject<{
3
+ url: z.ZodString;
4
+ request: z.ZodOptional<z.ZodEnum<{
5
+ POST: "POST";
6
+ GET: "GET";
7
+ PUT: "PUT";
8
+ DELETE: "DELETE";
9
+ PATCH: "PATCH";
10
+ HEAD: "HEAD";
11
+ OPTIONS: "OPTIONS";
12
+ }>>;
13
+ header: z.ZodOptional<z.ZodArray<z.ZodString>>;
14
+ data: z.ZodOptional<z.ZodArray<z.ZodString>>;
15
+ dataRaw: z.ZodOptional<z.ZodArray<z.ZodString>>;
16
+ dataAscii: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
+ dataBinary: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
+ dataUrlencode: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
+ json: z.ZodOptional<z.ZodString>;
20
+ form: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ formString: z.ZodOptional<z.ZodArray<z.ZodString>>;
22
+ get: z.ZodOptional<z.ZodBoolean>;
23
+ head: z.ZodOptional<z.ZodBoolean>;
24
+ location: z.ZodOptional<z.ZodBoolean>;
25
+ include: z.ZodOptional<z.ZodBoolean>;
26
+ output: z.ZodOptional<z.ZodString>;
27
+ remoteName: z.ZodOptional<z.ZodBoolean>;
28
+ verbose: z.ZodOptional<z.ZodBoolean>;
29
+ silent: z.ZodOptional<z.ZodBoolean>;
30
+ showError: z.ZodOptional<z.ZodBoolean>;
31
+ fail: z.ZodOptional<z.ZodBoolean>;
32
+ failWithBody: z.ZodOptional<z.ZodBoolean>;
33
+ writeOut: z.ZodOptional<z.ZodString>;
34
+ maxTime: z.ZodOptional<z.ZodNumber>;
35
+ user: z.ZodOptional<z.ZodString>;
36
+ compressed: z.ZodOptional<z.ZodBoolean>;
37
+ connectionId: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
38
+ }, z.core.$strip>;
39
+ export type CurlOptions = z.infer<typeof CurlSchema>;
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+ export const CurlSchema = z
3
+ .object({
4
+ url: z.string().describe("Request URL"),
5
+ request: z
6
+ .enum(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
7
+ .optional()
8
+ .describe("HTTP method (defaults to GET, or POST if data is provided)"),
9
+ header: z
10
+ .array(z.string())
11
+ .optional()
12
+ .describe("HTTP headers in 'Key: Value' format (repeatable)"),
13
+ data: z
14
+ .array(z.string())
15
+ .optional()
16
+ .describe("HTTP POST data (repeatable, joined with &)"),
17
+ dataRaw: z
18
+ .array(z.string())
19
+ .optional()
20
+ .describe("HTTP POST data without special interpretation (repeatable)"),
21
+ dataAscii: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("HTTP POST ASCII data (repeatable)"),
25
+ dataBinary: z
26
+ .array(z.string())
27
+ .optional()
28
+ .describe("HTTP POST binary data (repeatable)"),
29
+ dataUrlencode: z
30
+ .array(z.string())
31
+ .optional()
32
+ .describe("HTTP POST data, URL-encoded (repeatable)"),
33
+ json: z
34
+ .string()
35
+ .optional()
36
+ .describe("Send JSON body (sets Content-Type and Accept headers)"),
37
+ form: z
38
+ .array(z.string())
39
+ .optional()
40
+ .describe("Multipart form data as 'name=value' (repeatable)"),
41
+ formString: z
42
+ .array(z.string())
43
+ .optional()
44
+ .describe("Multipart form string field (repeatable)"),
45
+ get: z
46
+ .boolean()
47
+ .optional()
48
+ .describe("Force GET method and append data to query string"),
49
+ head: z.boolean().optional().describe("Fetch headers only (HEAD request)"),
50
+ location: z.boolean().optional().describe("Follow redirects"),
51
+ include: z
52
+ .boolean()
53
+ .optional()
54
+ .describe("Include response headers in output"),
55
+ output: z
56
+ .string()
57
+ .optional()
58
+ .describe("Write output to file instead of stdout"),
59
+ remoteName: z
60
+ .boolean()
61
+ .optional()
62
+ .describe("Write output to file named like the remote file"),
63
+ verbose: z
64
+ .boolean()
65
+ .optional()
66
+ .describe("Verbose output (show request/response headers on stderr)"),
67
+ silent: z.boolean().optional().describe("Silent mode (suppress errors)"),
68
+ showError: z
69
+ .boolean()
70
+ .optional()
71
+ .describe("Show errors even when in silent mode"),
72
+ fail: z
73
+ .boolean()
74
+ .optional()
75
+ .describe("Fail silently on HTTP errors (exit code 22)"),
76
+ failWithBody: z
77
+ .boolean()
78
+ .optional()
79
+ .describe("Fail on HTTP errors but still output the body"),
80
+ writeOut: z
81
+ .string()
82
+ .optional()
83
+ .describe("Output format string after completion (e.g., '%{http_code}')"),
84
+ maxTime: z
85
+ .number()
86
+ .optional()
87
+ .describe("Maximum time in seconds for the request"),
88
+ user: z
89
+ .string()
90
+ .optional()
91
+ .describe("Basic auth credentials as 'user:password'"),
92
+ compressed: z
93
+ .boolean()
94
+ .optional()
95
+ .describe("Request compressed response (sends Accept-Encoding header)"),
96
+ connectionId: z
97
+ .union([z.string(), z.number()])
98
+ .optional()
99
+ .describe("Zapier connection ID for authentication"),
100
+ })
101
+ .describe("Make HTTP requests through Zapier Relay with curl-like options");
@@ -0,0 +1,24 @@
1
+ export declare class CurlExitError extends Error {
2
+ exitCode: number;
3
+ constructor(message: string, exitCode: number);
4
+ }
5
+ export declare function parseHeaderLine(input: string): {
6
+ key: string;
7
+ value: string;
8
+ } | null;
9
+ export declare function basicAuthHeader(userpass: string): string;
10
+ export declare function deriveRemoteFilename(url: URL): string;
11
+ export declare function decodeWriteOutEscapes(input: string): string;
12
+ export declare function formatWriteOut(params: {
13
+ template: string;
14
+ urlEffective: string;
15
+ httpCode: number;
16
+ timeTotalSeconds: number;
17
+ sizeDownloadBytes: number;
18
+ contentType?: string | null;
19
+ }): string;
20
+ export declare function appendQueryParams(url: URL, dataParts: string[]): URL;
21
+ export declare function readAllStdin(): Promise<Buffer>;
22
+ export declare function resolveDataArgText(raw: string): Promise<string>;
23
+ export declare function resolveDataArgBinary(raw: string): Promise<Buffer>;
24
+ export declare function buildFormData(formArgs: string[], formStringArgs: string[]): Promise<FormData>;
@@ -0,0 +1,141 @@
1
+ import { createHash } from "crypto";
2
+ import { promises as fs } from "fs";
3
+ import { basename } from "path";
4
+ export class CurlExitError extends Error {
5
+ constructor(message, exitCode) {
6
+ super(message);
7
+ this.exitCode = exitCode;
8
+ this.name = "CurlExitError";
9
+ }
10
+ }
11
+ export function parseHeaderLine(input) {
12
+ const idx = input.indexOf(":");
13
+ if (idx === -1) {
14
+ return null;
15
+ }
16
+ const key = input.slice(0, idx).trim();
17
+ const value = input.slice(idx + 1).trim();
18
+ if (!key) {
19
+ return null;
20
+ }
21
+ return { key, value };
22
+ }
23
+ export function basicAuthHeader(userpass) {
24
+ const idx = userpass.indexOf(":");
25
+ const user = idx === -1 ? userpass : userpass.slice(0, idx);
26
+ const pass = idx === -1 ? "" : userpass.slice(idx + 1);
27
+ const token = Buffer.from(`${user}:${pass}`, "utf8").toString("base64");
28
+ return `Basic ${token}`;
29
+ }
30
+ export function deriveRemoteFilename(url) {
31
+ const path = url.pathname;
32
+ const candidate = path.endsWith("/") ? "index.html" : basename(path);
33
+ return candidate || "index.html";
34
+ }
35
+ export function decodeWriteOutEscapes(input) {
36
+ return input
37
+ .replace(/\\n/g, "\n")
38
+ .replace(/\\r/g, "\r")
39
+ .replace(/\\t/g, "\t");
40
+ }
41
+ export function formatWriteOut(params) {
42
+ const { template } = params;
43
+ const replacements = {
44
+ "%{http_code}": String(params.httpCode).padStart(3, "0"),
45
+ "%{time_total}": params.timeTotalSeconds.toFixed(3),
46
+ "%{size_download}": String(params.sizeDownloadBytes),
47
+ "%{url_effective}": params.urlEffective,
48
+ "%{content_type}": params.contentType ?? "",
49
+ };
50
+ let out = template;
51
+ for (const [token, value] of Object.entries(replacements)) {
52
+ out = out.split(token).join(value);
53
+ }
54
+ return decodeWriteOutEscapes(out);
55
+ }
56
+ export function appendQueryParams(url, dataParts) {
57
+ const out = new URL(url.toString());
58
+ for (const part of dataParts) {
59
+ const segments = part.split("&");
60
+ for (const seg of segments) {
61
+ if (!seg)
62
+ continue;
63
+ const idx = seg.indexOf("=");
64
+ if (idx === -1) {
65
+ out.searchParams.append(seg, "");
66
+ }
67
+ else {
68
+ out.searchParams.append(seg.slice(0, idx), seg.slice(idx + 1));
69
+ }
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+ export async function readAllStdin() {
75
+ const chunks = [];
76
+ for await (const chunk of process.stdin) {
77
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
78
+ }
79
+ return Buffer.concat(chunks);
80
+ }
81
+ export async function resolveDataArgText(raw) {
82
+ if (!raw.startsWith("@")) {
83
+ return raw;
84
+ }
85
+ const source = raw.slice(1);
86
+ if (source === "-") {
87
+ const buf = await readAllStdin();
88
+ return buf.toString("utf8");
89
+ }
90
+ const buf = await fs.readFile(source);
91
+ return buf.toString("utf8");
92
+ }
93
+ export async function resolveDataArgBinary(raw) {
94
+ if (!raw.startsWith("@")) {
95
+ return Buffer.from(raw, "utf8");
96
+ }
97
+ const source = raw.slice(1);
98
+ if (source === "-") {
99
+ return await readAllStdin();
100
+ }
101
+ return await fs.readFile(source);
102
+ }
103
+ export async function buildFormData(formArgs, formStringArgs) {
104
+ if (typeof FormData === "undefined") {
105
+ throw new CurlExitError("FormData is not available in this runtime; cannot use --form.", 2);
106
+ }
107
+ const fd = new FormData();
108
+ const addField = async (item, forceString) => {
109
+ const idx = item.indexOf("=");
110
+ if (idx === -1) {
111
+ throw new CurlExitError(`Invalid form field: '${item}'. Expected 'name=value' or 'name=@file'.`, 2);
112
+ }
113
+ const name = item.slice(0, idx);
114
+ const value = item.slice(idx + 1);
115
+ if (!name) {
116
+ throw new CurlExitError(`Invalid form field: '${item}'. Field name cannot be empty.`, 2);
117
+ }
118
+ if (!forceString && value.startsWith("@")) {
119
+ const filePath = value.slice(1);
120
+ const buf = filePath === "-" ? await readAllStdin() : await fs.readFile(filePath);
121
+ if (typeof Blob === "undefined") {
122
+ fd.append(name, buf.toString("utf8"));
123
+ return;
124
+ }
125
+ const blob = new Blob([buf]);
126
+ const filename = filePath === "-"
127
+ ? `stdin-${createHash("sha1").update(buf).digest("hex").slice(0, 8)}`
128
+ : basename(filePath);
129
+ fd.append(name, blob, filename);
130
+ return;
131
+ }
132
+ fd.append(name, value);
133
+ };
134
+ for (const item of formArgs) {
135
+ await addField(item, false);
136
+ }
137
+ for (const item of formStringArgs) {
138
+ await addField(item, true);
139
+ }
140
+ return fd;
141
+ }
@@ -7,3 +7,5 @@ export { addPlugin } from "./add";
7
7
  export { generateAppTypesPlugin } from "./generateAppTypes";
8
8
  export { buildManifestPlugin } from "./buildManifest";
9
9
  export { feedbackPlugin } from "./feedback";
10
+ export { curlPlugin } from "./curl";
11
+ export { cliOverridesPlugin } from "./cliOverrides";
@@ -7,3 +7,5 @@ export { addPlugin } from "./add";
7
7
  export { generateAppTypesPlugin } from "./generateAppTypes";
8
8
  export { buildManifestPlugin } from "./buildManifest";
9
9
  export { feedbackPlugin } from "./feedback";
10
+ export { curlPlugin } from "./curl";
11
+ export { cliOverridesPlugin } from "./cliOverrides";
package/dist/src/sdk.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createZapierSdkWithoutRegistry, registryPlugin, } from "@zapier/zapier-sdk";
2
- import { loginPlugin, logoutPlugin, mcpPlugin, bundleCodePlugin, getLoginConfigPathPlugin, addPlugin, generateAppTypesPlugin, buildManifestPlugin, feedbackPlugin, } from "./plugins/index";
2
+ import { loginPlugin, logoutPlugin, mcpPlugin, bundleCodePlugin, getLoginConfigPathPlugin, addPlugin, generateAppTypesPlugin, buildManifestPlugin, feedbackPlugin, curlPlugin, cliOverridesPlugin, } from "./plugins/index";
3
3
  /**
4
4
  * Create a Zapier SDK instance configured specifically for the CLI
5
5
  * Includes all CLI-specific plugins in addition to the standard SDK functionality
@@ -17,9 +17,12 @@ export function createZapierCliSdk(options = {}) {
17
17
  .addPlugin(getLoginConfigPathPlugin)
18
18
  .addPlugin(addPlugin)
19
19
  .addPlugin(feedbackPlugin)
20
+ .addPlugin(curlPlugin)
20
21
  .addPlugin(mcpPlugin)
21
22
  .addPlugin(loginPlugin)
22
23
  .addPlugin(logoutPlugin)
24
+ // Apply CLI-specific overrides (e.g., hide fetch in favor of curl)
25
+ .addPlugin(cliOverridesPlugin)
23
26
  // Add registry plugin to finalize SDK
24
27
  .addPlugin(registryPlugin));
25
28
  }