@zapier/zapier-sdk-cli 0.13.16 → 0.14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zapier/zapier-sdk-cli",
3
- "version": "0.13.16",
3
+ "version": "0.14.1",
4
4
  "description": "Command line interface for Zapier SDK",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -34,12 +34,15 @@
34
34
  "chalk": "^5.3.0",
35
35
  "cli-table3": "^0.6.5",
36
36
  "commander": "^12.0.0",
37
+ "conf": "^14.0.0",
37
38
  "esbuild": "^0.25.5",
38
39
  "express": "^5.1.0",
39
40
  "inquirer": "^12.6.3",
41
+ "is-installed-globally": "^1.0.0",
40
42
  "jsonwebtoken": "^9.0.2",
41
43
  "open": "^10.2.0",
42
44
  "ora": "^8.2.0",
45
+ "package-json": "^10.0.1",
43
46
  "pkce-challenge": "^5.0.0",
44
47
  "typescript": "^5.8.3",
45
48
  "zod": "^3.25.67",
package/src/cli.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { Command } from "commander";
3
+ import { Command, CommanderError } from "commander";
4
4
  import { generateCliCommands } from "./utils/cli-generator";
5
5
  import { createZapierCliSdk } from "./sdk";
6
6
  import packageJson from "../package.json" with { type: "json" };
7
+ import { ZapierCliError } from "./utils/errors";
8
+ import { checkAndNotifyUpdates } from "./utils/version-checker";
7
9
 
8
10
  const program = new Command();
9
11
 
@@ -61,4 +63,34 @@ generateCliCommands(program, sdk);
61
63
 
62
64
  // MCP command now handled by plugin
63
65
 
64
- program.parse();
66
+ // Override Commander's default exit behavior to handle our custom errors
67
+ program.exitOverride();
68
+
69
+ (async () => {
70
+ let exitCode = 0;
71
+
72
+ // Start version checking non-blocking
73
+ const versionCheckPromise = checkAndNotifyUpdates({
74
+ packageName: packageJson.name,
75
+ currentVersion: packageJson.version,
76
+ });
77
+
78
+ try {
79
+ await program.parseAsync();
80
+ } catch (error) {
81
+ if (error instanceof ZapierCliError) {
82
+ exitCode = error.exitCode;
83
+ } else if (error instanceof CommanderError) {
84
+ exitCode = error.exitCode;
85
+ } else {
86
+ // For any other unexpected errors, exit with code 1
87
+ console.error("Unexpected error:", error);
88
+ exitCode = 1;
89
+ }
90
+ }
91
+
92
+ // Wait for version checking to complete
93
+ await versionCheckPromise;
94
+
95
+ process.exit(exitCode);
96
+ })();
@@ -54,9 +54,6 @@ const loginWithSdk = createFunction(
54
54
  const user = await getLoggedInUser();
55
55
 
56
56
  console.log(`✅ Successfully logged in as ${user.email}`);
57
-
58
- // Force immediate exit to prevent hanging (especially in development with tsx)
59
- setTimeout(() => process.exit(0), 100);
60
57
  },
61
58
  );
62
59
 
@@ -20,6 +20,7 @@ import {
20
20
  getAuthTokenUrl,
21
21
  getAuthAuthorizeUrl,
22
22
  } from "@zapier/zapier-sdk-cli-login";
23
+ import { ZapierCliUserCancellationError } from "../errors";
23
24
 
24
25
  const findAvailablePort = (): Promise<number> => {
25
26
  return new Promise((resolve, reject) => {
@@ -91,8 +92,11 @@ const login = async ({
91
92
 
92
93
  log.info(`Using port ${availablePort} for OAuth callback`);
93
94
 
94
- const { promise: promisedCode, resolve: setCode } =
95
- getCallablePromise<string>();
95
+ const {
96
+ promise: promisedCode,
97
+ resolve: setCode,
98
+ reject: rejectCode,
99
+ } = getCallablePromise<string>();
96
100
 
97
101
  const app = express();
98
102
 
@@ -116,7 +120,7 @@ const login = async ({
116
120
  const cleanup = () => {
117
121
  server.close();
118
122
  log.info("\n❌ Login cancelled by user");
119
- process.exit(0);
123
+ rejectCode(new ZapierCliUserCancellationError());
120
124
  };
121
125
 
122
126
  process.on("SIGINT", cleanup);
@@ -11,6 +11,7 @@ import { SchemaParameterResolver } from "./parameter-resolver";
11
11
  import { formatItemsFromSchema, formatJsonOutput } from "./schema-formatter";
12
12
  import chalk from "chalk";
13
13
  import inquirer from "inquirer";
14
+ import { ZapierCliError, ZapierCliExitError } from "./errors";
14
15
 
15
16
  // ============================================================================
16
17
  // Types
@@ -387,20 +388,32 @@ function createCommandConfig(
387
388
  console.error(
388
389
  "\n" + chalk.dim(`Use --help to see available options`),
389
390
  );
391
+ throw new ZapierCliExitError("Validation failed", 1);
390
392
  } catch {
391
- console.error(chalk.red("Error:"), error.message);
393
+ console.error(
394
+ chalk.red("Error:"),
395
+ error instanceof Error ? error.message : String(error),
396
+ );
397
+ throw new ZapierCliExitError(
398
+ error instanceof Error ? error.message : String(error),
399
+ 1,
400
+ );
392
401
  }
402
+ } else if (error instanceof ZapierCliError) {
403
+ // Re-throw all CLI errors as-is
404
+ throw error;
393
405
  } else if (error instanceof ZapierError) {
394
406
  // Handle SDK errors with clean, user-friendly messages using our formatter
395
407
  const formattedMessage = formatErrorMessage(error);
396
408
  console.error(chalk.red("❌ Error:"), formattedMessage);
409
+ throw new ZapierCliExitError(formattedMessage, 1);
397
410
  } else {
398
411
  // Handle other errors
399
412
  const errorMessage =
400
413
  error instanceof Error ? error.message : "Unknown error";
401
414
  console.error(chalk.red("❌ Error:"), errorMessage);
415
+ throw new ZapierCliExitError(errorMessage, 1);
402
416
  }
403
- process.exit(1);
404
417
  }
405
418
  };
406
419
 
@@ -0,0 +1,26 @@
1
+ import { ZapierError } from "@zapier/zapier-sdk";
2
+
3
+ export abstract class ZapierCliError extends ZapierError {
4
+ public abstract readonly exitCode: number;
5
+ }
6
+
7
+ export class ZapierCliUserCancellationError extends ZapierCliError {
8
+ public readonly name = "ZapierCliUserCancellationError";
9
+ public readonly code = "ZAPIER_CLI_USER_CANCELLATION";
10
+ public readonly exitCode = 0;
11
+
12
+ constructor(message: string = "Operation cancelled by user") {
13
+ super(message);
14
+ }
15
+ }
16
+
17
+ export class ZapierCliExitError extends ZapierCliError {
18
+ public readonly name = "ZapierCliExitError";
19
+ public readonly code = "ZAPIER_CLI_EXIT";
20
+ public readonly exitCode: number;
21
+
22
+ constructor(message: string, exitCode: number = 1) {
23
+ super(message);
24
+ this.exitCode = exitCode;
25
+ }
26
+ }
package/src/utils/log.ts CHANGED
@@ -13,6 +13,11 @@ const log = {
13
13
  warn: (message: string, ...args: unknown[]) => {
14
14
  console.log(chalk.yellow("⚠"), message, ...args);
15
15
  },
16
+ debug: (message: string, ...args: unknown[]) => {
17
+ if (process.env.DEBUG === "true" || process.argv.includes("--debug")) {
18
+ console.log(chalk.gray("🐛"), message, ...args);
19
+ }
20
+ },
16
21
  };
17
22
 
18
23
  export default log;
@@ -0,0 +1,83 @@
1
+ import { existsSync } from "fs";
2
+ import { join } from "path";
3
+ import isInstalledGlobally from "is-installed-globally";
4
+
5
+ export interface PackageManagerInfo {
6
+ name: "npm" | "yarn" | "pnpm" | "bun" | "unknown";
7
+ source: "runtime" | "lockfile" | "fallback";
8
+ }
9
+
10
+ /**
11
+ * Detect which package manager is being used or configured for this project.
12
+ *
13
+ * Returns an object like:
14
+ * { name: 'npm' | 'yarn' | 'pnpm' | 'bun' | 'unknown', source: 'runtime' | 'lockfile' | 'fallback' }
15
+ */
16
+ export function detectPackageManager(cwd = process.cwd()): PackageManagerInfo {
17
+ // --- 1. Runtime detection (based on env)
18
+ const ua = process.env.npm_config_user_agent;
19
+ if (ua) {
20
+ if (ua.includes("yarn")) return { name: "yarn", source: "runtime" };
21
+ if (ua.includes("pnpm")) return { name: "pnpm", source: "runtime" };
22
+ if (ua.includes("bun")) return { name: "bun", source: "runtime" };
23
+ if (ua.includes("npm")) return { name: "npm", source: "runtime" };
24
+ }
25
+
26
+ // --- 2. Lockfile detection (based on files in cwd)
27
+ const files: Array<[string, PackageManagerInfo["name"]]> = [
28
+ ["pnpm-lock.yaml", "pnpm"],
29
+ ["yarn.lock", "yarn"],
30
+ ["bun.lockb", "bun"],
31
+ ["package-lock.json", "npm"],
32
+ ];
33
+
34
+ for (const [file, name] of files) {
35
+ if (existsSync(join(cwd, file))) {
36
+ return { name, source: "lockfile" };
37
+ }
38
+ }
39
+
40
+ // --- 3. Fallback
41
+ return { name: "unknown", source: "fallback" };
42
+ }
43
+
44
+ /**
45
+ * Get the appropriate update command for the detected package manager.
46
+ * Returns global update commands if globally installed, local commands if locally installed.
47
+ */
48
+ export function getUpdateCommand(packageName: string): string {
49
+ const pm = detectPackageManager();
50
+ const isGlobal = isInstalledGlobally;
51
+
52
+ if (isGlobal) {
53
+ // Global update commands
54
+ switch (pm.name) {
55
+ case "yarn":
56
+ return `yarn global upgrade ${packageName}@latest`;
57
+ case "pnpm":
58
+ return `pnpm update -g ${packageName}@latest`;
59
+ case "bun":
60
+ return `bun update -g ${packageName}@latest`;
61
+ case "npm":
62
+ return `npm update -g ${packageName}@latest`;
63
+ case "unknown":
64
+ // Default to npm since it's most widely supported
65
+ return `npm update -g ${packageName}@latest`;
66
+ }
67
+ } else {
68
+ // Local update commands
69
+ switch (pm.name) {
70
+ case "yarn":
71
+ return `yarn upgrade ${packageName}@latest`;
72
+ case "pnpm":
73
+ return `pnpm update ${packageName}@latest`;
74
+ case "bun":
75
+ return `bun update ${packageName}@latest`;
76
+ case "npm":
77
+ return `npm update ${packageName}@latest`;
78
+ case "unknown":
79
+ // Default to npm since it's most widely supported
80
+ return `npm update ${packageName}@latest`;
81
+ }
82
+ }
83
+ }
@@ -2,6 +2,7 @@ import inquirer from "inquirer";
2
2
  import chalk from "chalk";
3
3
  import { z } from "zod";
4
4
  import type { ZapierSdk } from "@zapier/zapier-sdk";
5
+ import { ZapierCliUserCancellationError } from "./errors";
5
6
 
6
7
  // ============================================================================
7
8
  // Types
@@ -209,7 +210,7 @@ export class SchemaParameterResolver {
209
210
  } catch (error) {
210
211
  if (this.isUserCancellation(error)) {
211
212
  console.log(chalk.yellow("\n\nOperation cancelled by user"));
212
- process.exit(0);
213
+ throw new ZapierCliUserCancellationError();
213
214
  }
214
215
  throw error;
215
216
  }
@@ -257,7 +258,7 @@ export class SchemaParameterResolver {
257
258
  } catch (error) {
258
259
  if (this.isUserCancellation(error)) {
259
260
  console.log(chalk.yellow("\n\nOperation cancelled by user"));
260
- process.exit(0);
261
+ throw new ZapierCliUserCancellationError();
261
262
  }
262
263
  throw error;
263
264
  }
@@ -302,7 +303,7 @@ export class SchemaParameterResolver {
302
303
  } catch (error) {
303
304
  if (this.isUserCancellation(error)) {
304
305
  console.log(chalk.yellow("\n\nOperation cancelled by user"));
305
- process.exit(0);
306
+ throw new ZapierCliUserCancellationError();
306
307
  }
307
308
  throw error;
308
309
  }
@@ -670,7 +671,7 @@ export class SchemaParameterResolver {
670
671
  } catch (error) {
671
672
  if (this.isUserCancellation(error)) {
672
673
  console.log(chalk.yellow("\n\nOperation cancelled by user"));
673
- process.exit(0);
674
+ throw new ZapierCliUserCancellationError();
674
675
  }
675
676
  throw error;
676
677
  }
@@ -755,7 +756,7 @@ export class SchemaParameterResolver {
755
756
  } catch (error) {
756
757
  if (this.isUserCancellation(error)) {
757
758
  console.log(chalk.yellow("\n\nOperation cancelled by user"));
758
- process.exit(0);
759
+ throw new ZapierCliUserCancellationError();
759
760
  }
760
761
  throw error;
761
762
  }
@@ -1,4 +1,5 @@
1
1
  import ora from "ora";
2
+ import { ZapierCliUserCancellationError } from "./errors";
2
3
 
3
4
  export const spinPromise = async <T>(
4
5
  promise: Promise<T>,
@@ -10,7 +11,13 @@ export const spinPromise = async <T>(
10
11
  spinner.succeed();
11
12
  return result;
12
13
  } catch (error) {
13
- spinner.fail();
14
+ if (error instanceof ZapierCliUserCancellationError) {
15
+ // For user cancellation, just stop the spinner without showing failure
16
+ spinner.stop();
17
+ } else {
18
+ // For actual errors, show failure
19
+ spinner.fail();
20
+ }
14
21
  throw error;
15
22
  }
16
23
  };
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { checkForUpdates, displayUpdateNotification } from "./version-checker";
3
+
4
+ // Mock package-json
5
+ vi.mock("package-json", () => ({
6
+ default: vi.fn(),
7
+ }));
8
+
9
+ // Mock chalk to make testing easier
10
+ vi.mock("chalk", () => ({
11
+ default: {
12
+ red: Object.assign((text: string) => text, {
13
+ bold: (text: string) => text,
14
+ }),
15
+ yellow: Object.assign((text: string) => text, {
16
+ bold: (text: string) => text,
17
+ }),
18
+ bold: (text: string) => text,
19
+ },
20
+ }));
21
+
22
+ // Mock log module
23
+ vi.mock("./log", () => ({
24
+ default: {
25
+ debug: vi.fn(),
26
+ },
27
+ }));
28
+
29
+ // Mock conf module
30
+ const mockConf = {
31
+ get: vi.fn(),
32
+ set: vi.fn(),
33
+ };
34
+ vi.mock("conf", () => ({
35
+ default: vi.fn().mockImplementation(() => mockConf),
36
+ }));
37
+
38
+ const mockPackageJson = vi.mocked(await import("package-json")).default;
39
+
40
+ describe("version-checker", () => {
41
+ beforeEach(() => {
42
+ vi.clearAllMocks();
43
+ // Ensure cache is cleared between tests by returning undefined from get
44
+ mockConf.get.mockReturnValue(undefined);
45
+ // Suppress console output during tests
46
+ vi.spyOn(console, "log").mockImplementation(() => {});
47
+ vi.spyOn(console, "error").mockImplementation(() => {});
48
+ });
49
+
50
+ describe("checkForUpdates", () => {
51
+ it("should detect when an update is available", async () => {
52
+ mockPackageJson.mockResolvedValue({
53
+ version: "1.0.1",
54
+ deprecated: false,
55
+ });
56
+
57
+ const result = await checkForUpdates({
58
+ packageName: "test-package",
59
+ currentVersion: "1.0.0",
60
+ });
61
+
62
+ expect(result).toEqual({
63
+ hasUpdate: true,
64
+ latestVersion: "1.0.1",
65
+ currentVersion: "1.0.0",
66
+ isDeprecated: false,
67
+ deprecationMessage: undefined,
68
+ });
69
+ });
70
+
71
+ it("should detect when package is deprecated", async () => {
72
+ mockPackageJson.mockResolvedValue({
73
+ version: "1.0.0",
74
+ deprecated: "This package is no longer maintained",
75
+ });
76
+
77
+ const result = await checkForUpdates({
78
+ packageName: "test-package",
79
+ currentVersion: "1.0.0",
80
+ });
81
+
82
+ expect(result).toEqual({
83
+ hasUpdate: false,
84
+ latestVersion: "1.0.0",
85
+ currentVersion: "1.0.0",
86
+ isDeprecated: true,
87
+ deprecationMessage: "This package is no longer maintained",
88
+ });
89
+ });
90
+
91
+ it("should handle when no update is available", async () => {
92
+ mockPackageJson.mockResolvedValue({
93
+ version: "1.0.0",
94
+ deprecated: false,
95
+ });
96
+
97
+ const result = await checkForUpdates({
98
+ packageName: "test-package",
99
+ currentVersion: "1.0.0",
100
+ });
101
+
102
+ expect(result).toEqual({
103
+ hasUpdate: false,
104
+ latestVersion: "1.0.0",
105
+ currentVersion: "1.0.0",
106
+ isDeprecated: false,
107
+ deprecationMessage: undefined,
108
+ });
109
+ });
110
+
111
+ it("should handle network errors gracefully", async () => {
112
+ mockPackageJson.mockRejectedValue(new Error("Network error"));
113
+
114
+ const result = await checkForUpdates({
115
+ packageName: "test-package",
116
+ currentVersion: "1.0.0",
117
+ });
118
+
119
+ expect(result).toEqual({
120
+ hasUpdate: false,
121
+ currentVersion: "1.0.0",
122
+ isDeprecated: false,
123
+ });
124
+ });
125
+ });
126
+
127
+ describe("displayUpdateNotification", () => {
128
+ it("should display deprecation warning", () => {
129
+ const consoleSpy = vi
130
+ .spyOn(console, "error")
131
+ .mockImplementation(() => {});
132
+
133
+ displayUpdateNotification(
134
+ {
135
+ hasUpdate: false,
136
+ currentVersion: "1.0.0",
137
+ isDeprecated: true,
138
+ deprecationMessage: "This package is deprecated",
139
+ },
140
+ "test-package",
141
+ );
142
+
143
+ expect(consoleSpy).toHaveBeenCalledWith(
144
+ expect.stringContaining("DEPRECATION WARNING"),
145
+ );
146
+ expect(consoleSpy).toHaveBeenCalledWith(
147
+ expect.stringContaining("This package is deprecated"),
148
+ );
149
+ });
150
+
151
+ it("should display update notification", () => {
152
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
153
+
154
+ displayUpdateNotification(
155
+ {
156
+ hasUpdate: true,
157
+ currentVersion: "1.0.0",
158
+ latestVersion: "1.0.1",
159
+ isDeprecated: false,
160
+ },
161
+ "test-package",
162
+ );
163
+
164
+ expect(consoleSpy).toHaveBeenCalledWith(
165
+ expect.stringContaining("Update available!"),
166
+ );
167
+ expect(consoleSpy).toHaveBeenCalledWith(
168
+ expect.stringContaining("pnpm update test-package"),
169
+ );
170
+ });
171
+
172
+ it("should display both deprecation and update notifications", () => {
173
+ const consoleErrorSpy = vi
174
+ .spyOn(console, "error")
175
+ .mockImplementation(() => {});
176
+ const consoleLogSpy = vi
177
+ .spyOn(console, "log")
178
+ .mockImplementation(() => {});
179
+
180
+ displayUpdateNotification(
181
+ {
182
+ hasUpdate: true,
183
+ currentVersion: "1.0.0",
184
+ latestVersion: "1.0.1",
185
+ isDeprecated: true,
186
+ deprecationMessage: "This package is deprecated",
187
+ },
188
+ "test-package",
189
+ );
190
+
191
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
192
+ expect.stringContaining("DEPRECATION WARNING"),
193
+ );
194
+ expect(consoleLogSpy).toHaveBeenCalledWith(
195
+ expect.stringContaining("Update available!"),
196
+ );
197
+ });
198
+ });
199
+ });