@zapier/zapier-sdk-cli 0.15.1 → 0.15.3

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.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "Command line interface for Zapier SDK",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -47,9 +47,9 @@
47
47
  "semver": "^7.7.3",
48
48
  "typescript": "^5.8.3",
49
49
  "zod": "^3.25.67",
50
- "@zapier/zapier-sdk-cli-login": "0.3.4",
51
- "@zapier/zapier-sdk": "0.15.3",
52
- "@zapier/zapier-sdk-mcp": "0.3.27"
50
+ "@zapier/zapier-sdk-mcp": "0.3.28",
51
+ "@zapier/zapier-sdk": "0.15.4",
52
+ "@zapier/zapier-sdk-cli-login": "0.3.4"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/express": "^5.0.3",
@@ -67,6 +67,8 @@ export const loginPlugin: Plugin<
67
67
  const startTime = Date.now();
68
68
  let success = false;
69
69
  let errorMessage: string | null = null;
70
+ let accountId: number | null = null;
71
+ let customUserId: number | null = null;
70
72
 
71
73
  try {
72
74
  await loginWithSdk({
@@ -76,6 +78,13 @@ export const loginPlugin: Plugin<
76
78
  authClientId: context.options?.authClientId,
77
79
  });
78
80
  success = true;
81
+
82
+ // Extract user IDs after successful login
83
+ try {
84
+ ({ accountId, customUserId } = await getLoggedInUser());
85
+ } catch {
86
+ // If we can't get user info, continue without it
87
+ }
79
88
  } catch (error) {
80
89
  success = false;
81
90
  errorMessage = error instanceof Error ? error.message : "Login failed";
@@ -101,6 +110,8 @@ export const loginPlugin: Plugin<
101
110
  selected_api: context.selected_api,
102
111
  app_id: context.app_id,
103
112
  app_version_id: context.app_version_id,
113
+ customuser_id: customUserId,
114
+ account_id: accountId,
104
115
  },
105
116
  cliVersion: cliPackageJson.version,
106
117
  });
@@ -1,7 +1,7 @@
1
1
  import inquirer from "inquirer";
2
2
  import chalk from "chalk";
3
3
  import { z } from "zod";
4
- import type { ZapierSdk } from "@zapier/zapier-sdk";
4
+ import type { ZapierSdk, ActionTypeProperty } from "@zapier/zapier-sdk";
5
5
  import { ZapierCliUserCancellationError } from "./errors";
6
6
 
7
7
  // ============================================================================
@@ -23,6 +23,17 @@ interface ResolverContext {
23
23
  functionName?: string;
24
24
  }
25
25
 
26
+ interface FieldMetadata {
27
+ key: string;
28
+ title: string;
29
+ description?: string;
30
+ isRequired: boolean;
31
+ defaultValue?: unknown;
32
+ valueType: string;
33
+ hasDropdown: boolean;
34
+ isMultiSelect: boolean;
35
+ }
36
+
26
37
  // ============================================================================
27
38
  // Local Resolution Helper Functions
28
39
  // ============================================================================
@@ -501,6 +512,7 @@ export class SchemaParameterResolver {
501
512
  processedFieldKeys,
502
513
  [],
503
514
  iteration,
515
+ updatedContext,
504
516
  );
505
517
 
506
518
  // If no new fields were processed, we're done
@@ -535,6 +547,7 @@ export class SchemaParameterResolver {
535
547
  processedFieldKeys: Set<string>,
536
548
  fieldsetPath: string[] = [],
537
549
  iteration: number = 1,
550
+ context?: ResolverContext,
538
551
  ): Promise<{
539
552
  newRequired: number;
540
553
  newOptional: number;
@@ -581,6 +594,7 @@ export class SchemaParameterResolver {
581
594
  processedFieldKeys,
582
595
  nestedPath,
583
596
  iteration,
597
+ context,
584
598
  );
585
599
 
586
600
  newRequiredCount += nestedStats.newRequired;
@@ -608,7 +622,7 @@ export class SchemaParameterResolver {
608
622
  );
609
623
  }
610
624
 
611
- await this.promptForField(typedItem, targetInputs);
625
+ await this.promptForField(typedItem, targetInputs, context);
612
626
  processedFieldKeys.add(typedItem.key);
613
627
  } else {
614
628
  // Collect optional fields for batch processing
@@ -656,7 +670,7 @@ export class SchemaParameterResolver {
656
670
  if (shouldConfigureOptional.configure) {
657
671
  console.log(chalk.cyan(`\nOptional fields${pathContext}:`));
658
672
  for (const field of optionalFields) {
659
- await this.promptForField(field, targetInputs);
673
+ await this.promptForField(field, targetInputs, context);
660
674
  const typedField = field as { key: string };
661
675
  processedFieldKeys.add(typedField.key);
662
676
  }
@@ -704,54 +718,244 @@ export class SchemaParameterResolver {
704
718
  parent[lastKey] = value;
705
719
  }
706
720
 
707
- private async promptForField(
708
- field: unknown,
709
- inputs: Record<string, unknown>,
710
- ): Promise<void> {
721
+ /**
722
+ * Extract and normalize field metadata from raw field object
723
+ */
724
+ private extractFieldMetadata(field: unknown): FieldMetadata {
711
725
  const fieldObj = field as {
712
726
  type?: string;
713
727
  key: string;
728
+ title?: string;
714
729
  label?: string;
715
730
  is_required?: boolean;
731
+ description?: string;
716
732
  helpText?: string;
733
+ default_value?: unknown;
717
734
  default?: unknown;
718
- choices?: Array<{ label?: string; value: unknown }>;
735
+ value_type?: string;
736
+ format?: string;
737
+ items?: { type: string };
719
738
  };
720
739
 
721
- const fieldPrompt: Record<string, unknown> = {
722
- type: fieldObj.type === "boolean" ? "confirm" : "input",
723
- name: fieldObj.key,
724
- message: `${fieldObj.label || fieldObj.key}${fieldObj.is_required ? " (required)" : " (optional)"}:`,
740
+ const valueType = fieldObj.value_type || "string";
741
+
742
+ return {
743
+ key: fieldObj.key,
744
+ title: fieldObj.title || fieldObj.label || fieldObj.key,
745
+ description: fieldObj.description || fieldObj.helpText,
746
+ isRequired: fieldObj.is_required || false,
747
+ defaultValue: fieldObj.default_value ?? fieldObj.default,
748
+ valueType,
749
+ hasDropdown: fieldObj.format === "SELECT",
750
+ isMultiSelect: Boolean(
751
+ valueType === "array" ||
752
+ (fieldObj.items && fieldObj.items.type !== undefined),
753
+ ),
725
754
  };
755
+ }
756
+
757
+ /**
758
+ * Fetch a page of choices for a dropdown field
759
+ */
760
+ private async fetchChoices(
761
+ fieldMeta: FieldMetadata,
762
+ inputs: Record<string, unknown>,
763
+ context: ResolverContext,
764
+ cursor?: string,
765
+ ): Promise<{
766
+ choices: Array<{ label: string; value: unknown }>;
767
+ nextCursor?: string;
768
+ }> {
769
+ try {
770
+ console.log(
771
+ chalk.gray(
772
+ cursor
773
+ ? ` Fetching more choices...`
774
+ : ` Fetching choices for ${fieldMeta.title}...`,
775
+ ),
776
+ );
777
+
778
+ const page = await context.sdk.listInputFieldChoices({
779
+ appKey: context.resolvedParams.appKey as string,
780
+ actionKey: context.resolvedParams.actionKey as string,
781
+ actionType: context.resolvedParams.actionType as ActionTypeProperty,
782
+ authenticationId: context.resolvedParams.authenticationId as
783
+ | number
784
+ | null,
785
+ inputFieldKey: fieldMeta.key,
786
+ inputs,
787
+ ...(cursor && { cursor }),
788
+ });
789
+
790
+ const choices = page.data.map((choice) => ({
791
+ label: choice.label || choice.key || String(choice.value),
792
+ value: choice.value ?? choice.key,
793
+ }));
794
+
795
+ if (choices.length === 0 && !cursor) {
796
+ console.log(
797
+ chalk.yellow(` No choices available for ${fieldMeta.title}`),
798
+ );
799
+ }
800
+
801
+ return {
802
+ choices,
803
+ nextCursor: page.nextCursor,
804
+ };
805
+ } catch (error) {
806
+ console.warn(
807
+ chalk.yellow(` ⚠️ Failed to fetch choices for ${fieldMeta.title}:`),
808
+ error,
809
+ );
810
+ return { choices: [] };
811
+ }
812
+ }
813
+
814
+ /**
815
+ * Prompt user with choices (handles both single and multi-select with pagination)
816
+ */
817
+ private async promptWithChoices({
818
+ fieldMeta,
819
+ choices: initialChoices,
820
+ nextCursor: initialCursor,
821
+ inputs,
822
+ context,
823
+ }: {
824
+ fieldMeta: FieldMetadata;
825
+ choices: Array<{ label: string; value: unknown }>;
826
+ nextCursor?: string;
827
+ inputs: Record<string, unknown>;
828
+ context?: ResolverContext;
829
+ }): Promise<unknown> {
830
+ const choices = [...initialChoices];
831
+ let nextCursor = initialCursor;
832
+ const LOAD_MORE_SENTINEL = Symbol("LOAD_MORE");
833
+
834
+ // Progressive loading loop
835
+ while (true) {
836
+ const promptChoices = choices.map((choice) => ({
837
+ name: choice.label,
838
+ value: choice.value,
839
+ }));
840
+
841
+ // Add "(Load more...)" option if there are more pages
842
+ if (nextCursor) {
843
+ promptChoices.push({
844
+ name: chalk.dim("(Load more...)"),
845
+ value: LOAD_MORE_SENTINEL,
846
+ });
847
+ }
848
+
849
+ // Add skip option for optional fields (single-select only)
850
+ if (!fieldMeta.isRequired && !fieldMeta.isMultiSelect) {
851
+ promptChoices.push({ name: "(Skip)", value: undefined });
852
+ }
853
+
854
+ const promptConfig = {
855
+ type: fieldMeta.isMultiSelect ? "checkbox" : "list",
856
+ name: fieldMeta.key,
857
+ message: `${fieldMeta.title}${fieldMeta.isRequired ? " (required)" : " (optional)"}:`,
858
+ choices: promptChoices,
859
+ ...(fieldMeta.isMultiSelect && {
860
+ validate: (input: unknown[]) => {
861
+ if (fieldMeta.isRequired && (!input || input.length === 0)) {
862
+ return "At least one selection is required";
863
+ }
864
+ return true;
865
+ },
866
+ }),
867
+ };
868
+
869
+ const answer = await inquirer.prompt([promptConfig as any]);
870
+ let selectedValue = answer[fieldMeta.key];
871
+
872
+ // Check if user selected "Load more..."
873
+ const wantsMore = fieldMeta.isMultiSelect
874
+ ? Array.isArray(selectedValue) &&
875
+ selectedValue.includes(LOAD_MORE_SENTINEL)
876
+ : selectedValue === LOAD_MORE_SENTINEL;
877
+
878
+ if (wantsMore && nextCursor && context) {
879
+ // Remove sentinel from multi-select
880
+ if (fieldMeta.isMultiSelect && Array.isArray(selectedValue)) {
881
+ selectedValue = selectedValue.filter((v) => v !== LOAD_MORE_SENTINEL);
882
+ }
726
883
 
727
- if (fieldObj.helpText) {
728
- fieldPrompt.prefix = chalk.gray(`ℹ ${fieldObj.helpText}\n`);
884
+ // Fetch next page
885
+ const result = await this.fetchChoices(
886
+ fieldMeta,
887
+ inputs,
888
+ context,
889
+ nextCursor,
890
+ );
891
+ choices.push(...result.choices);
892
+ nextCursor = result.nextCursor;
893
+
894
+ // Re-prompt with updated choices
895
+ continue;
896
+ }
897
+
898
+ return selectedValue;
729
899
  }
900
+ }
730
901
 
731
- if (fieldObj.default !== undefined) {
732
- fieldPrompt.default = fieldObj.default;
902
+ /**
903
+ * Prompt user for free-form input (text or boolean)
904
+ */
905
+ private async promptFreeForm(fieldMeta: FieldMetadata): Promise<unknown> {
906
+ const promptConfig: Record<string, unknown> = {
907
+ name: fieldMeta.key,
908
+ message: `${fieldMeta.title}${fieldMeta.isRequired ? " (required)" : " (optional)"}:`,
909
+ };
910
+
911
+ if (fieldMeta.valueType === "boolean") {
912
+ promptConfig.type = "confirm";
913
+ promptConfig.default =
914
+ fieldMeta.defaultValue !== undefined
915
+ ? Boolean(fieldMeta.defaultValue)
916
+ : undefined;
917
+ } else {
918
+ promptConfig.type = "input";
919
+ promptConfig.default = fieldMeta.defaultValue;
920
+ promptConfig.validate = (input: string) => {
921
+ if (fieldMeta.isRequired && !input) {
922
+ return "This field is required";
923
+ }
924
+ return true;
925
+ };
733
926
  }
734
927
 
735
- if (fieldObj.choices && fieldObj.choices.length > 0) {
736
- fieldPrompt.type = "list";
737
- (fieldPrompt as { choices: unknown[] }).choices = fieldObj.choices.map(
738
- (choice: unknown) => {
739
- const choiceObj = choice as { label?: unknown; value: unknown };
740
- return {
741
- name: choiceObj.label || choiceObj.value,
742
- value: choiceObj.value,
743
- };
744
- },
745
- );
928
+ // Add help text if available
929
+ if (fieldMeta.description) {
930
+ promptConfig.prefix = chalk.gray(`ℹ ${fieldMeta.description}\n`);
746
931
  }
747
932
 
748
933
  try {
749
- const answer = await inquirer.prompt([fieldPrompt as any]);
934
+ const answer = await inquirer.prompt([promptConfig as any]);
935
+ return answer[fieldMeta.key];
936
+ } catch (error) {
937
+ if (this.isUserCancellation(error)) {
938
+ console.log(chalk.yellow("\n\nOperation cancelled by user"));
939
+ throw new ZapierCliUserCancellationError();
940
+ }
941
+ throw error;
942
+ }
943
+ }
750
944
 
751
- if (answer[fieldObj.key] !== undefined && answer[fieldObj.key] !== "") {
752
- inputs[fieldObj.key] = answer[fieldObj.key];
753
- } else if (fieldObj.is_required) {
754
- throw new Error(`Required field ${fieldObj.key} cannot be empty`);
945
+ /**
946
+ * Store field value in inputs object with validation
947
+ */
948
+ private storeFieldValue(
949
+ inputs: Record<string, unknown>,
950
+ key: string,
951
+ value: unknown,
952
+ isRequired: boolean,
953
+ ): void {
954
+ try {
955
+ if (value !== undefined && value !== "") {
956
+ inputs[key] = value;
957
+ } else if (isRequired) {
958
+ throw new Error(`Required field ${key} cannot be empty`);
755
959
  }
756
960
  } catch (error) {
757
961
  if (this.isUserCancellation(error)) {
@@ -762,6 +966,45 @@ export class SchemaParameterResolver {
762
966
  }
763
967
  }
764
968
 
969
+ private async promptForField(
970
+ field: unknown,
971
+ inputs: Record<string, unknown>,
972
+ context?: ResolverContext,
973
+ ): Promise<void> {
974
+ const fieldMeta = this.extractFieldMetadata(field);
975
+
976
+ // Fetch choices if field has dropdown
977
+ let choices: Array<{ label: string; value: unknown }> = [];
978
+ let nextCursor: string | undefined;
979
+ if (fieldMeta.hasDropdown && context) {
980
+ const result = await this.fetchChoices(fieldMeta, inputs, context);
981
+ choices = result.choices;
982
+ nextCursor = result.nextCursor;
983
+ }
984
+
985
+ // Prompt user based on field type
986
+ let selectedValue: unknown;
987
+ if (choices.length > 0) {
988
+ selectedValue = await this.promptWithChoices({
989
+ fieldMeta,
990
+ choices,
991
+ nextCursor,
992
+ inputs,
993
+ context,
994
+ });
995
+ } else {
996
+ selectedValue = await this.promptFreeForm(fieldMeta);
997
+ }
998
+
999
+ // Store result
1000
+ this.storeFieldValue(
1001
+ inputs,
1002
+ fieldMeta.key,
1003
+ selectedValue,
1004
+ fieldMeta.isRequired,
1005
+ );
1006
+ }
1007
+
765
1008
  private isUserCancellation(error: unknown): boolean {
766
1009
  const errorObj = error as {
767
1010
  name?: string;