@zapier/zapier-sdk-cli 0.32.4 → 0.34.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +39 -2
  3. package/dist/cli.cjs +589 -110
  4. package/dist/cli.mjs +580 -102
  5. package/dist/index.cjs +531 -18
  6. package/dist/index.mjs +525 -15
  7. package/dist/package.json +3 -2
  8. package/dist/src/cli.js +11 -1
  9. package/dist/src/paths.d.ts +1 -0
  10. package/dist/src/paths.js +6 -0
  11. package/dist/src/plugins/index.d.ts +1 -0
  12. package/dist/src/plugins/index.js +1 -0
  13. package/dist/src/plugins/init/display.d.ts +9 -0
  14. package/dist/src/plugins/init/display.js +72 -0
  15. package/dist/src/plugins/init/index.d.ts +16 -0
  16. package/dist/src/plugins/init/index.js +61 -0
  17. package/dist/src/plugins/init/schemas.d.ts +8 -0
  18. package/dist/src/plugins/init/schemas.js +14 -0
  19. package/dist/src/plugins/init/steps.d.ts +39 -0
  20. package/dist/src/plugins/init/steps.js +141 -0
  21. package/dist/src/plugins/init/types.d.ts +31 -0
  22. package/dist/src/plugins/init/types.js +1 -0
  23. package/dist/src/plugins/init/utils.d.ts +48 -0
  24. package/dist/src/plugins/init/utils.js +135 -0
  25. package/dist/src/plugins/logout/index.js +1 -1
  26. package/dist/src/sdk.js +5 -2
  27. package/dist/src/utils/auth/login.js +33 -4
  28. package/dist/src/utils/package-manager-detector.d.ts +3 -1
  29. package/dist/src/utils/package-manager-detector.js +1 -0
  30. package/dist/src/utils/version-checker.js +1 -8
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +6 -5
  33. package/templates/basic/AGENTS.md.hbs +15 -0
  34. package/templates/basic/README.md.hbs +21 -0
  35. package/templates/basic/package.json.hbs +17 -0
  36. package/templates/basic/src/index.ts +46 -0
  37. package/templates/basic/tsconfig.json +12 -0
@@ -0,0 +1,9 @@
1
+ import type { DisplayHooks, RunnableSetupStep, SetupStepId } from "./types";
2
+ import type { PackageManager } from "../../utils/package-manager-detector";
3
+ export declare function createConsoleDisplayHooks(): DisplayHooks;
4
+ export declare function displaySummaryAndNextSteps({ projectName, steps, completedSetupStepIds, packageManager, }: {
5
+ projectName: string;
6
+ steps: RunnableSetupStep[];
7
+ completedSetupStepIds: SetupStepId[];
8
+ packageManager: PackageManager;
9
+ }): void;
@@ -0,0 +1,72 @@
1
+ import chalk from "chalk";
2
+ import { buildNextSteps } from "./steps";
3
+ import { getPackageManagerCommands } from "./utils";
4
+ export function createConsoleDisplayHooks() {
5
+ return {
6
+ onItemComplete: (message) => console.log(" " + chalk.green("✓") + " " + chalk.dim(message)),
7
+ onWarn: (message) => console.warn(chalk.yellow("!") + " " + message),
8
+ onStepStart: ({ description, stepNumber, totalSteps, command, skipPrompts, }) => {
9
+ const progressMessage = `${description}...`;
10
+ const stepCounter = chalk.dim(`${stepNumber}/${totalSteps}`);
11
+ if (skipPrompts) {
12
+ console.log("\n" +
13
+ chalk.bold(`❯ ${progressMessage}`) +
14
+ " " +
15
+ stepCounter +
16
+ "\n");
17
+ }
18
+ else {
19
+ console.log(chalk.dim("→") + " " + progressMessage + " " + stepCounter);
20
+ }
21
+ if (command) {
22
+ console.log(" " + chalk.cyan(`$ ${command}`));
23
+ }
24
+ },
25
+ onStepSuccess: ({ stepNumber, totalSteps }) => console.log("\n" +
26
+ chalk.green("✓") +
27
+ " " +
28
+ chalk.dim(`Step ${stepNumber}/${totalSteps} complete`) +
29
+ "\n"),
30
+ onStepError: ({ description, command, err }) => {
31
+ const detail = err instanceof Error && err.message
32
+ ? `\n ${chalk.dim(err.message)}`
33
+ : "";
34
+ const hint = command
35
+ ? `\n ${chalk.dim("run manually:")} ${chalk.cyan(`$ ${command}`)}`
36
+ : "";
37
+ console.error(`\n${chalk.red("✖")} ${chalk.bold(description)}${chalk.dim(" failed")}${detail}${hint}`);
38
+ },
39
+ };
40
+ }
41
+ export function displaySummaryAndNextSteps({ projectName, steps, completedSetupStepIds, packageManager, }) {
42
+ const formatStatus = (complete) => ({
43
+ icon: complete ? chalk.green("✓") : chalk.yellow("!"),
44
+ text: complete
45
+ ? chalk.green("Setup complete")
46
+ : chalk.yellow("Setup interrupted"),
47
+ });
48
+ const formatNextStep = (step, i) => " " + chalk.dim(`${i + 1}.`) + " " + chalk.bold(step.description);
49
+ const formatCommand = (cmd) => " " + chalk.cyan(`$ ${cmd}`);
50
+ const formatCompletedStep = (step) => " " + chalk.green("✓") + " " + step.description;
51
+ const { execCmd } = getPackageManagerCommands({ packageManager });
52
+ const leftoverSteps = steps.filter((s) => !completedSetupStepIds.includes(s.id));
53
+ const isComplete = leftoverSteps.length === 0;
54
+ const status = formatStatus(isComplete);
55
+ console.log("\n" + chalk.bold("❯ Summary") + "\n");
56
+ console.log(" " + chalk.dim("Project") + " " + chalk.bold(projectName));
57
+ console.log(" " + chalk.dim("Status") + " " + status.icon + " " + status.text);
58
+ const completedSteps = steps.filter((s) => completedSetupStepIds.includes(s.id));
59
+ if (completedSteps.length > 0) {
60
+ console.log();
61
+ for (const step of completedSteps)
62
+ console.log(formatCompletedStep(step));
63
+ }
64
+ const nextSteps = buildNextSteps({ projectName, leftoverSteps, execCmd });
65
+ console.log("\n" + chalk.bold("❯ Next Steps") + "\n");
66
+ nextSteps.forEach((step, i) => {
67
+ console.log(formatNextStep(step, i));
68
+ if (step.command)
69
+ console.log(formatCommand(step.command));
70
+ console.log();
71
+ });
72
+ }
@@ -0,0 +1,16 @@
1
+ import type { Plugin } from "@zapier/zapier-sdk";
2
+ import { InitSchema, type InitOptions } from "./schemas";
3
+ export { toProjectName } from "./utils";
4
+ interface InitPluginProvides {
5
+ init: (options: InitOptions) => Promise<void>;
6
+ context: {
7
+ meta: {
8
+ init: {
9
+ inputSchema: typeof InitSchema;
10
+ categories: string[];
11
+ };
12
+ };
13
+ };
14
+ }
15
+ export declare const initPlugin: Plugin<{}, {}, InitPluginProvides>;
16
+ export type { InitPluginProvides };
@@ -0,0 +1,61 @@
1
+ import { createFunction } from "@zapier/zapier-sdk";
2
+ import { InitSchema } from "./schemas";
3
+ import { detectPackageManager } from "../../utils/package-manager-detector";
4
+ import { validateInitOptions, withInterruptCleanup } from "./utils";
5
+ import { getInitSteps, runStep } from "./steps";
6
+ import { createConsoleDisplayHooks, displaySummaryAndNextSteps, } from "./display";
7
+ import { ZapierCliExitError } from "../../utils/errors";
8
+ export { toProjectName } from "./utils";
9
+ export const initPlugin = () => {
10
+ const init = createFunction(async function init(options) {
11
+ const { projectName: rawName, skipPrompts = false } = options;
12
+ const cwd = process.cwd();
13
+ const { projectName, projectDir } = validateInitOptions({ rawName, cwd });
14
+ const displayHooks = createConsoleDisplayHooks();
15
+ const packageManagerInfo = detectPackageManager(cwd);
16
+ if (packageManagerInfo.name === "unknown") {
17
+ displayHooks.onWarn("Could not detect package manager, defaulting to npm.");
18
+ }
19
+ const packageManager = packageManagerInfo.name === "unknown" ? "npm" : packageManagerInfo.name;
20
+ const steps = getInitSteps({
21
+ projectDir,
22
+ projectName,
23
+ packageManager,
24
+ displayHooks,
25
+ });
26
+ const completedSetupStepIds = [];
27
+ for (let i = 0; i < steps.length; i++) {
28
+ const step = steps[i];
29
+ const succeeded = await withInterruptCleanup(step.cleanup, () => runStep({
30
+ step,
31
+ stepNumber: i + 1,
32
+ totalSteps: steps.length,
33
+ skipPrompts,
34
+ displayHooks,
35
+ }));
36
+ if (!succeeded)
37
+ break;
38
+ completedSetupStepIds.push(step.id);
39
+ }
40
+ if (completedSetupStepIds.length === 0) {
41
+ throw new ZapierCliExitError("Project setup failed — no steps completed.");
42
+ }
43
+ displaySummaryAndNextSteps({
44
+ projectName,
45
+ steps,
46
+ completedSetupStepIds,
47
+ packageManager,
48
+ });
49
+ }, InitSchema);
50
+ return {
51
+ init,
52
+ context: {
53
+ meta: {
54
+ init: {
55
+ categories: ["utility"],
56
+ inputSchema: InitSchema,
57
+ },
58
+ },
59
+ },
60
+ };
61
+ };
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ export declare const TEMPLATES: readonly ["basic"];
3
+ export type TemplateName = (typeof TEMPLATES)[number];
4
+ export declare const InitSchema: z.ZodObject<{
5
+ projectName: z.ZodString;
6
+ skipPrompts: z.ZodOptional<z.ZodBoolean>;
7
+ }, z.core.$strip>;
8
+ export type InitOptions = z.infer<typeof InitSchema>;
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ export const TEMPLATES = ["basic"];
3
+ export const InitSchema = z
4
+ .object({
5
+ projectName: z
6
+ .string()
7
+ .min(1)
8
+ .describe("Name of the project directory to create"),
9
+ skipPrompts: z
10
+ .boolean()
11
+ .optional()
12
+ .describe("Skip all interactive prompts and accept all defaults"),
13
+ })
14
+ .describe("Create a new Zapier SDK project in a new directory with starter files");
@@ -0,0 +1,39 @@
1
+ import { type TemplateName } from "./schemas";
2
+ import { type PackageManager } from "../../utils/package-manager-detector";
3
+ import type { DisplayHooks, SetupStep, SetupStepId, RunnableSetupStep } from "./types";
4
+ export type { DisplayHooks, SetupStep, SetupStepId, RunnableSetupStep };
5
+ export declare function getTemplateDir({ template, }?: {
6
+ template?: TemplateName;
7
+ }): string;
8
+ export declare function scaffoldFiles({ projectDir, templatesDir, variables, displayHooks, }: {
9
+ projectDir: string;
10
+ templatesDir: string;
11
+ variables?: Record<string, unknown>;
12
+ displayHooks?: DisplayHooks;
13
+ }): void;
14
+ export declare function scaffoldProject({ projectDir, projectName, packageManager, template, displayHooks, }: {
15
+ projectDir: string;
16
+ projectName: string;
17
+ packageManager: PackageManager;
18
+ template?: TemplateName;
19
+ displayHooks?: DisplayHooks;
20
+ }): void;
21
+ export declare function runStep({ step, stepNumber, totalSteps, skipPrompts, displayHooks, }: {
22
+ step: RunnableSetupStep;
23
+ stepNumber: number;
24
+ totalSteps: number;
25
+ skipPrompts: boolean;
26
+ displayHooks?: DisplayHooks;
27
+ }): Promise<boolean>;
28
+ export declare function getInitSteps({ projectDir, projectName, packageManager, template, displayHooks, }: {
29
+ projectDir: string;
30
+ projectName: string;
31
+ packageManager: PackageManager;
32
+ template?: TemplateName;
33
+ displayHooks?: DisplayHooks;
34
+ }): RunnableSetupStep[];
35
+ export declare function buildNextSteps({ projectName, leftoverSteps, execCmd, }: {
36
+ projectName: string;
37
+ leftoverSteps: RunnableSetupStep[];
38
+ execCmd: string;
39
+ }): SetupStep[];
@@ -0,0 +1,141 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, copyFileSync, } from "fs";
2
+ import { join, dirname, relative } from "path";
3
+ import { promptYesNo, createExec, getPackageManagerCommands, getDirentParentPath, buildTemplateVariables, cleanupProject, createProjectDir, isTemplateFile, getDestRelPath, renderTemplate, } from "./utils";
4
+ import { TEMPLATES } from "./schemas";
5
+ import { TEMPLATES_DIR } from "../../paths";
6
+ const DEFAULT_TEMPLATE = "basic";
7
+ export function getTemplateDir({ template = DEFAULT_TEMPLATE, } = {}) {
8
+ const dirPath = join(TEMPLATES_DIR, template);
9
+ if (!existsSync(dirPath)) {
10
+ throw new Error(`Template "${template}" not found at ${dirPath}. Available templates: ${TEMPLATES.join(", ")}`);
11
+ }
12
+ return dirPath;
13
+ }
14
+ export function scaffoldFiles({ projectDir, templatesDir, variables = {}, displayHooks, }) {
15
+ const entries = readdirSync(templatesDir, {
16
+ withFileTypes: true,
17
+ recursive: true,
18
+ });
19
+ const files = entries.filter((e) => e.isFile());
20
+ if (files.length === 0) {
21
+ throw new Error(`Template directory "${templatesDir}" contains no files.`);
22
+ }
23
+ for (const entry of files) {
24
+ const srcPath = join(getDirentParentPath(entry), entry.name);
25
+ const relPath = relative(templatesDir, srcPath);
26
+ const isTemplate = isTemplateFile(entry.name);
27
+ const destRelPath = getDestRelPath(relPath, isTemplate);
28
+ const destPath = join(projectDir, destRelPath);
29
+ mkdirSync(dirname(destPath), { recursive: true });
30
+ if (isTemplate) {
31
+ writeFileSync(destPath, renderTemplate(srcPath, variables));
32
+ }
33
+ else {
34
+ copyFileSync(srcPath, destPath);
35
+ }
36
+ displayHooks?.onItemComplete?.(destRelPath);
37
+ }
38
+ }
39
+ export function scaffoldProject({ projectDir, projectName, packageManager, template, displayHooks, }) {
40
+ const variables = buildTemplateVariables({ projectName, packageManager });
41
+ const templatesDir = getTemplateDir({ template });
42
+ createProjectDir({ projectDir, projectName });
43
+ scaffoldFiles({ projectDir, templatesDir, variables, displayHooks });
44
+ }
45
+ export async function runStep({ step, stepNumber, totalSteps, skipPrompts, displayHooks, }) {
46
+ if (step.askConfirmation) {
47
+ const should = await promptYesNo({
48
+ message: step.description,
49
+ defaultValue: true,
50
+ skipPrompts,
51
+ });
52
+ if (!should)
53
+ return false;
54
+ }
55
+ displayHooks?.onStepStart({
56
+ description: step.description,
57
+ stepNumber,
58
+ totalSteps,
59
+ command: step.command,
60
+ skipPrompts,
61
+ });
62
+ try {
63
+ await step.run();
64
+ displayHooks?.onStepSuccess({ stepNumber, totalSteps });
65
+ return true;
66
+ }
67
+ catch (err) {
68
+ step.cleanup?.();
69
+ displayHooks?.onStepError({
70
+ description: step.description,
71
+ command: step.command,
72
+ err,
73
+ });
74
+ return false;
75
+ }
76
+ }
77
+ export function getInitSteps({ projectDir, projectName, packageManager, template, displayHooks, }) {
78
+ if (template !== undefined &&
79
+ !TEMPLATES.includes(template)) {
80
+ throw new Error(`Unknown template "${template}". Available templates: ${TEMPLATES.join(", ")}`);
81
+ }
82
+ const { installCmd, execCmd, devCmd } = getPackageManagerCommands({
83
+ packageManager,
84
+ });
85
+ const exec = createExec({ cwd: projectDir });
86
+ return [
87
+ {
88
+ id: "scaffold",
89
+ description: "Scaffold project files",
90
+ cleanup: () => cleanupProject({ projectDir }),
91
+ run: () => scaffoldProject({
92
+ projectDir,
93
+ projectName,
94
+ packageManager,
95
+ template,
96
+ displayHooks,
97
+ }),
98
+ },
99
+ {
100
+ id: "install-deps",
101
+ description: "Install dependencies",
102
+ askConfirmation: true,
103
+ command: installCmd,
104
+ run: () => exec(installCmd),
105
+ },
106
+ {
107
+ id: "login",
108
+ description: "Log in to Zapier",
109
+ askConfirmation: true,
110
+ command: `${execCmd} zapier-sdk login`,
111
+ run: () => exec(`${execCmd} zapier-sdk login`),
112
+ },
113
+ {
114
+ id: "run",
115
+ description: "Run your app",
116
+ askConfirmation: true,
117
+ command: devCmd,
118
+ run: () => exec(devCmd),
119
+ },
120
+ ];
121
+ }
122
+ export function buildNextSteps({ projectName, leftoverSteps, execCmd, }) {
123
+ return [
124
+ {
125
+ description: "Change into your project directory",
126
+ command: `cd ${projectName}`,
127
+ },
128
+ ...leftoverSteps.map(({ description, command }) => ({
129
+ description,
130
+ command,
131
+ })),
132
+ {
133
+ description: "Search for an app to integrate",
134
+ command: `${execCmd} zapier-sdk list-apps --search "<app name>"`,
135
+ },
136
+ {
137
+ description: "Add an app and generate TypeScript types",
138
+ command: `${execCmd} zapier-sdk add <app-key>`,
139
+ },
140
+ ];
141
+ }
@@ -0,0 +1,31 @@
1
+ export interface DisplayHooks {
2
+ onItemComplete: (message: string) => void;
3
+ onWarn: (message: string) => void;
4
+ onStepStart: (params: {
5
+ description: string;
6
+ stepNumber: number;
7
+ totalSteps: number;
8
+ command?: string;
9
+ skipPrompts: boolean;
10
+ }) => void;
11
+ onStepSuccess: (params: {
12
+ stepNumber: number;
13
+ totalSteps: number;
14
+ }) => void;
15
+ onStepError: (params: {
16
+ description: string;
17
+ command?: string;
18
+ err: unknown;
19
+ }) => void;
20
+ }
21
+ export interface SetupStep {
22
+ description: string;
23
+ command?: string;
24
+ }
25
+ export type SetupStepId = "scaffold" | "install-deps" | "login" | "run";
26
+ export interface RunnableSetupStep extends SetupStep {
27
+ id: SetupStepId;
28
+ askConfirmation?: boolean;
29
+ cleanup?: () => void;
30
+ run: () => void | Promise<void>;
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import type { Dirent } from "fs";
2
+ import { type PackageManager } from "../../utils/package-manager-detector";
3
+ export declare function getDirentParentPath(entry: Dirent): string;
4
+ export declare function toProjectName(name: string): string;
5
+ export declare function validateInitOptions({ rawName, cwd, }: {
6
+ rawName: string;
7
+ cwd: string;
8
+ }): {
9
+ projectName: string;
10
+ projectDir: string;
11
+ };
12
+ export declare function createExec({ cwd }: {
13
+ cwd: string;
14
+ }): (cmd: string) => void;
15
+ export declare function promptYesNo({ message, defaultValue, skipPrompts, }: {
16
+ message: string;
17
+ defaultValue: boolean;
18
+ skipPrompts: boolean;
19
+ }): Promise<boolean>;
20
+ export declare function getPackageManagerCommands({ packageManager, }: {
21
+ packageManager: PackageManager;
22
+ }): {
23
+ installCmd: string;
24
+ devCmd: string;
25
+ devScript: string;
26
+ execCmd: string;
27
+ };
28
+ export declare function isTemplateFile(name: string): boolean;
29
+ export declare function getDestRelPath(relPath: string, isTemplate: boolean): string;
30
+ export declare function renderTemplate(srcPath: string, variables: Record<string, unknown>): string;
31
+ export declare function buildTemplateVariables({ projectName, packageManager, }: {
32
+ projectName: string;
33
+ packageManager: PackageManager;
34
+ }): Record<string, unknown>;
35
+ export declare function cleanupProject({ projectDir }: {
36
+ projectDir: string;
37
+ }): void;
38
+ /**
39
+ * Registers a cleanup callback that fires if the process receives SIGINT while
40
+ * `fn` is running. The listener is always removed once `fn` settles, preventing
41
+ * listener leaks across steps. The SIGINT is re-raised after cleanup so the
42
+ * shell receives a proper signal-terminated exit code (130) rather than exit(1).
43
+ */
44
+ export declare function withInterruptCleanup(cleanup: (() => void) | undefined, fn: () => Promise<boolean>): Promise<boolean>;
45
+ export declare function createProjectDir({ projectDir, projectName, }: {
46
+ projectDir: string;
47
+ projectName: string;
48
+ }): void;
@@ -0,0 +1,135 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "fs";
2
+ import { execSync } from "child_process";
3
+ import { extname, join } from "path";
4
+ import chalk from "chalk";
5
+ import Handlebars from "handlebars";
6
+ import inquirer from "inquirer";
7
+ // Node 20 added `parentPath`; Node 18 used `path` for the same purpose.
8
+ export function getDirentParentPath(entry) {
9
+ const e = entry;
10
+ const parent = e.parentPath ?? e.path;
11
+ if (!parent)
12
+ throw new Error("readdirSync entry missing parentPath/path — unsupported Node version");
13
+ return parent;
14
+ }
15
+ export function toProjectName(name) {
16
+ // Essentially converts to kebab-case
17
+ return (name
18
+ .toLowerCase()
19
+ // Replace anything that isn't a letter, digit, or hyphen with a hyphen.
20
+ // Hyphens are kept as-is since they're valid in npm package names.
21
+ .replace(/[^a-z0-9-]/g, "-")
22
+ .replace(/-+/g, "-") // collapse consecutive hyphens produced by the step above
23
+ .replace(/^-|-$/g, "")); // strip any leading/trailing hyphens
24
+ }
25
+ export function validateInitOptions({ rawName, cwd, }) {
26
+ const projectName = toProjectName(rawName);
27
+ if (!projectName) {
28
+ throw new Error(`"${rawName}" results in an empty project name. Provide a name with at least one letter or number.`);
29
+ }
30
+ const projectDir = join(cwd, projectName);
31
+ if (existsSync(projectDir)) {
32
+ const contents = readdirSync(projectDir);
33
+ if (contents.length > 0) {
34
+ throw new Error(`Directory "${projectName}" already exists and is not empty. Choose a different name.`);
35
+ }
36
+ }
37
+ return { projectName, projectDir };
38
+ }
39
+ // shell is required on Windows where npx/npm/pnpm/yarn are .cmd files
40
+ // that execSync cannot locate without a shell.
41
+ export function createExec({ cwd }) {
42
+ return (cmd) => execSync(cmd, {
43
+ cwd,
44
+ stdio: "inherit",
45
+ shell: process.platform === "win32" ? "cmd.exe" : "/bin/sh",
46
+ });
47
+ }
48
+ export async function promptYesNo({ message, defaultValue, skipPrompts, }) {
49
+ if (skipPrompts)
50
+ return defaultValue;
51
+ const { answer } = await inquirer.prompt([
52
+ { type: "confirm", name: "answer", message, default: defaultValue },
53
+ ]);
54
+ return answer;
55
+ }
56
+ export function getPackageManagerCommands({ packageManager, }) {
57
+ return {
58
+ installCmd: `${packageManager} install`,
59
+ devCmd: packageManager === "yarn" ? "yarn dev" : `${packageManager} run dev`,
60
+ devScript: packageManager === "bun" ? "bun src/index.ts" : "tsx src/index.ts",
61
+ execCmd: {
62
+ npm: "npx",
63
+ yarn: "yarn dlx",
64
+ pnpm: "pnpm dlx",
65
+ bun: "bunx",
66
+ }[packageManager],
67
+ };
68
+ }
69
+ export function isTemplateFile(name) {
70
+ return extname(name) === ".hbs";
71
+ }
72
+ export function getDestRelPath(relPath, isTemplate) {
73
+ return isTemplate ? relPath.slice(0, -extname(relPath).length) : relPath;
74
+ }
75
+ export function renderTemplate(srcPath, variables) {
76
+ const source = readFileSync(srcPath, "utf-8");
77
+ // Without this, Handlebars HTML-encodes output (& → &amp;), which corrupts JSON and TypeScript files.
78
+ const template = Handlebars.compile(source, { noEscape: true });
79
+ return template(variables);
80
+ }
81
+ export function buildTemplateVariables({ projectName, packageManager, }) {
82
+ const { execCmd, devScript, devCmd } = getPackageManagerCommands({
83
+ packageManager,
84
+ });
85
+ return {
86
+ projectName,
87
+ execCmd,
88
+ devScript,
89
+ devCmd,
90
+ includeTsx: packageManager !== "bun",
91
+ };
92
+ }
93
+ export function cleanupProject({ projectDir }) {
94
+ console.log("\n" + chalk.yellow("!") + " Cleaning up...");
95
+ rmSync(projectDir, { recursive: true, force: true });
96
+ }
97
+ /**
98
+ * Registers a cleanup callback that fires if the process receives SIGINT while
99
+ * `fn` is running. The listener is always removed once `fn` settles, preventing
100
+ * listener leaks across steps. The SIGINT is re-raised after cleanup so the
101
+ * shell receives a proper signal-terminated exit code (130) rather than exit(1).
102
+ */
103
+ export async function withInterruptCleanup(cleanup, fn) {
104
+ if (!cleanup)
105
+ return fn();
106
+ const handler = () => {
107
+ cleanup();
108
+ process.removeListener("SIGINT", handler);
109
+ process.kill(process.pid, "SIGINT");
110
+ };
111
+ process.on("SIGINT", handler);
112
+ try {
113
+ return await fn();
114
+ }
115
+ finally {
116
+ process.removeListener("SIGINT", handler);
117
+ }
118
+ }
119
+ export function createProjectDir({ projectDir, projectName, }) {
120
+ try {
121
+ mkdirSync(projectDir, { recursive: true });
122
+ }
123
+ catch (err) {
124
+ if (err instanceof Error) {
125
+ const code = err.code;
126
+ if (code === "EACCES") {
127
+ throw new Error(`Permission denied creating "${projectName}". Check directory permissions.`);
128
+ }
129
+ if (code === "ENOSPC") {
130
+ throw new Error("No space left on device.");
131
+ }
132
+ }
133
+ throw err;
134
+ }
135
+ }
@@ -2,7 +2,7 @@ import { createFunction } from "@zapier/zapier-sdk";
2
2
  import { logout } from "@zapier/zapier-sdk-cli-login";
3
3
  import { LogoutSchema } from "./schemas";
4
4
  const logoutWithSdk = createFunction(async function logoutWithSdk(_options) {
5
- logout();
5
+ await logout();
6
6
  console.log("✅ Successfully logged out");
7
7
  }, LogoutSchema);
8
8
  export const logoutPlugin = () => ({
package/dist/src/sdk.js CHANGED
@@ -1,5 +1,7 @@
1
- import { createZapierSdkWithoutRegistry, registryPlugin, } from "@zapier/zapier-sdk";
2
- import { loginPlugin, logoutPlugin, mcpPlugin, bundleCodePlugin, getLoginConfigPathPlugin, addPlugin, generateAppTypesPlugin, buildManifestPlugin, feedbackPlugin, curlPlugin, cliOverridesPlugin, } from "./plugins/index";
1
+ import * as cliLogin from "@zapier/zapier-sdk-cli-login";
2
+ import { createZapierSdkWithoutRegistry, injectCliLogin, registryPlugin, } from "@zapier/zapier-sdk";
3
+ import { loginPlugin, logoutPlugin, mcpPlugin, bundleCodePlugin, getLoginConfigPathPlugin, addPlugin, generateAppTypesPlugin, buildManifestPlugin, feedbackPlugin, curlPlugin, cliOverridesPlugin, initPlugin, } from "./plugins/index";
4
+ injectCliLogin(cliLogin);
3
5
  /**
4
6
  * Create a Zapier SDK instance configured specifically for the CLI
5
7
  * Includes all CLI-specific plugins in addition to the standard SDK functionality
@@ -19,6 +21,7 @@ export function createZapierCliSdk(options = {}) {
19
21
  .addPlugin(addPlugin)
20
22
  .addPlugin(feedbackPlugin)
21
23
  .addPlugin(curlPlugin)
24
+ .addPlugin(initPlugin)
22
25
  .addPlugin(mcpPlugin)
23
26
  .addPlugin(loginPlugin)
24
27
  .addPlugin(logoutPlugin)
@@ -7,7 +7,8 @@ import { spinPromise } from "../spinner";
7
7
  import log from "../log";
8
8
  import api from "../api/client";
9
9
  import getCallablePromise from "../getCallablePromise";
10
- import { updateLogin, logout, getPkceLoginConfig, } from "@zapier/zapier-sdk-cli-login";
10
+ import inquirer from "inquirer";
11
+ import { updateLogin, logout, getPkceLoginConfig, getLoginStorageMode, getConfigPath, } from "@zapier/zapier-sdk-cli-login";
11
12
  import { ZapierCliUserCancellationError } from "../errors";
12
13
  const findAvailablePort = () => {
13
14
  return new Promise((resolve, reject) => {
@@ -58,8 +59,7 @@ const login = async ({ timeoutMs = LOGIN_TIMEOUT_MS, credentials, }) => {
58
59
  credentials,
59
60
  });
60
61
  const scope = ensureOfflineAccess(credentials?.scope || "internal credentials");
61
- // Force logout
62
- logout();
62
+ await logout();
63
63
  // Find an available port
64
64
  const availablePort = await findAvailablePort();
65
65
  const redirectUri = `http://localhost:${availablePort}/oauth`;
@@ -147,7 +147,36 @@ const login = async ({ timeoutMs = LOGIN_TIMEOUT_MS, credentials, }) => {
147
147
  "Content-Type": "application/x-www-form-urlencoded",
148
148
  },
149
149
  });
150
- updateLogin(data);
150
+ let targetStorage;
151
+ if (getLoginStorageMode() === "config") {
152
+ const { upgrade } = await inquirer.prompt([
153
+ {
154
+ type: "confirm",
155
+ name: "upgrade",
156
+ message: "Would you like to upgrade to system keychain storage? " +
157
+ "This is recommended to securely store your credentials. " +
158
+ "However, note that older SDK/CLI versions will NOT be able to read these credentials, " +
159
+ "so you will want to upgrade them to the latest version.",
160
+ default: true,
161
+ },
162
+ ]);
163
+ targetStorage = upgrade ? "keychain" : "config";
164
+ }
165
+ else {
166
+ targetStorage = "keychain";
167
+ }
168
+ try {
169
+ await updateLogin(data, { storage: targetStorage });
170
+ }
171
+ catch (err) {
172
+ if (targetStorage === "keychain") {
173
+ log.warn(`Could not store credentials in system keychain. Storing in plaintext at ${getConfigPath()}.`);
174
+ await updateLogin(data, { storage: "config" });
175
+ }
176
+ else {
177
+ throw err;
178
+ }
179
+ }
151
180
  log.info("Token exchange completed successfully");
152
181
  return data.access_token;
153
182
  };