@zapier/zapier-sdk-cli 0.59.1 → 0.59.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.
Files changed (174) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/cli.cjs +51 -36
  3. package/dist/cli.mjs +53 -38
  4. package/dist/experimental.cjs +11 -3
  5. package/dist/experimental.mjs +12 -4
  6. package/dist/index.cjs +12 -4
  7. package/dist/index.mjs +13 -5
  8. package/package.json +3 -3
  9. package/dist/package.json +0 -121
  10. package/dist/src/cli.d.ts +0 -2
  11. package/dist/src/cli.js +0 -219
  12. package/dist/src/experimental.d.ts +0 -33
  13. package/dist/src/experimental.js +0 -86
  14. package/dist/src/generators/ast-generator.d.ts +0 -41
  15. package/dist/src/generators/ast-generator.js +0 -405
  16. package/dist/src/index.d.ts +0 -5
  17. package/dist/src/index.js +0 -5
  18. package/dist/src/login/config.d.ts +0 -4
  19. package/dist/src/login/config.js +0 -21
  20. package/dist/src/login/credentials-revoke.d.ts +0 -14
  21. package/dist/src/login/credentials-revoke.js +0 -95
  22. package/dist/src/login/credentials-store.d.ts +0 -60
  23. package/dist/src/login/credentials-store.js +0 -181
  24. package/dist/src/login/filesystem-cache.d.ts +0 -25
  25. package/dist/src/login/filesystem-cache.js +0 -195
  26. package/dist/src/login/index.d.ts +0 -110
  27. package/dist/src/login/index.js +0 -424
  28. package/dist/src/login/keychain.d.ts +0 -18
  29. package/dist/src/login/keychain.js +0 -74
  30. package/dist/src/login/legacy-jwt.d.ts +0 -4
  31. package/dist/src/login/legacy-jwt.js +0 -18
  32. package/dist/src/login.d.ts +0 -10
  33. package/dist/src/login.js +0 -10
  34. package/dist/src/paths.d.ts +0 -1
  35. package/dist/src/paths.js +0 -6
  36. package/dist/src/plugins/add/index.d.ts +0 -254
  37. package/dist/src/plugins/add/index.js +0 -95
  38. package/dist/src/plugins/add/schemas.d.ts +0 -8
  39. package/dist/src/plugins/add/schemas.js +0 -22
  40. package/dist/src/plugins/auth/credentials-base-url.d.ts +0 -11
  41. package/dist/src/plugins/auth/credentials-base-url.js +0 -24
  42. package/dist/src/plugins/buildManifest/index.d.ts +0 -120
  43. package/dist/src/plugins/buildManifest/index.js +0 -71
  44. package/dist/src/plugins/buildManifest/schemas.d.ts +0 -44
  45. package/dist/src/plugins/buildManifest/schemas.js +0 -17
  46. package/dist/src/plugins/bundleCode/index.d.ts +0 -23
  47. package/dist/src/plugins/bundleCode/index.js +0 -62
  48. package/dist/src/plugins/bundleCode/schemas.d.ts +0 -10
  49. package/dist/src/plugins/bundleCode/schemas.js +0 -19
  50. package/dist/src/plugins/cliOverrides/index.d.ts +0 -21
  51. package/dist/src/plugins/cliOverrides/index.js +0 -17
  52. package/dist/src/plugins/curl/index.d.ts +0 -76
  53. package/dist/src/plugins/curl/index.js +0 -236
  54. package/dist/src/plugins/curl/schemas.d.ts +0 -40
  55. package/dist/src/plugins/curl/schemas.js +0 -108
  56. package/dist/src/plugins/curl/utils.d.ts +0 -34
  57. package/dist/src/plugins/curl/utils.js +0 -150
  58. package/dist/src/plugins/drainTriggerInbox/index.d.ts +0 -46
  59. package/dist/src/plugins/drainTriggerInbox/index.js +0 -178
  60. package/dist/src/plugins/feedback/index.d.ts +0 -24
  61. package/dist/src/plugins/feedback/index.js +0 -52
  62. package/dist/src/plugins/feedback/schemas.d.ts +0 -5
  63. package/dist/src/plugins/feedback/schemas.js +0 -8
  64. package/dist/src/plugins/generateAppTypes/index.d.ts +0 -265
  65. package/dist/src/plugins/generateAppTypes/index.js +0 -129
  66. package/dist/src/plugins/generateAppTypes/schemas.d.ts +0 -58
  67. package/dist/src/plugins/generateAppTypes/schemas.js +0 -21
  68. package/dist/src/plugins/getLoginConfigPath/index.d.ts +0 -16
  69. package/dist/src/plugins/getLoginConfigPath/index.js +0 -9
  70. package/dist/src/plugins/getLoginConfigPath/schemas.d.ts +0 -3
  71. package/dist/src/plugins/getLoginConfigPath/schemas.js +0 -5
  72. package/dist/src/plugins/index.d.ts +0 -15
  73. package/dist/src/plugins/index.js +0 -15
  74. package/dist/src/plugins/init/display.d.ts +0 -9
  75. package/dist/src/plugins/init/display.js +0 -72
  76. package/dist/src/plugins/init/index.d.ts +0 -21
  77. package/dist/src/plugins/init/index.js +0 -56
  78. package/dist/src/plugins/init/schemas.d.ts +0 -9
  79. package/dist/src/plugins/init/schemas.js +0 -19
  80. package/dist/src/plugins/init/steps.d.ts +0 -39
  81. package/dist/src/plugins/init/steps.js +0 -141
  82. package/dist/src/plugins/init/types.d.ts +0 -31
  83. package/dist/src/plugins/init/types.js +0 -1
  84. package/dist/src/plugins/init/utils.d.ts +0 -48
  85. package/dist/src/plugins/init/utils.js +0 -135
  86. package/dist/src/plugins/login/index.d.ts +0 -25
  87. package/dist/src/plugins/login/index.js +0 -12
  88. package/dist/src/plugins/login/schemas.d.ts +0 -9
  89. package/dist/src/plugins/login/schemas.js +0 -27
  90. package/dist/src/plugins/logout/index.d.ts +0 -30
  91. package/dist/src/plugins/logout/index.js +0 -33
  92. package/dist/src/plugins/logout/schemas.d.ts +0 -2
  93. package/dist/src/plugins/logout/schemas.js +0 -4
  94. package/dist/src/plugins/mcp/index.d.ts +0 -28
  95. package/dist/src/plugins/mcp/index.js +0 -24
  96. package/dist/src/plugins/mcp/schemas.d.ts +0 -5
  97. package/dist/src/plugins/mcp/schemas.js +0 -10
  98. package/dist/src/plugins/signup/index.d.ts +0 -25
  99. package/dist/src/plugins/signup/index.js +0 -12
  100. package/dist/src/plugins/signup/schemas.d.ts +0 -9
  101. package/dist/src/plugins/signup/schemas.js +0 -26
  102. package/dist/src/plugins/signup/test-harness.d.ts +0 -34
  103. package/dist/src/plugins/signup/test-harness.js +0 -76
  104. package/dist/src/plugins/watchTriggerInbox/index.d.ts +0 -46
  105. package/dist/src/plugins/watchTriggerInbox/index.js +0 -166
  106. package/dist/src/sdk.d.ts +0 -17
  107. package/dist/src/sdk.js +0 -67
  108. package/dist/src/telemetry/builders.d.ts +0 -44
  109. package/dist/src/telemetry/builders.js +0 -59
  110. package/dist/src/telemetry/events.d.ts +0 -40
  111. package/dist/src/telemetry/events.js +0 -4
  112. package/dist/src/types/sdk.d.ts +0 -6
  113. package/dist/src/types/sdk.js +0 -1
  114. package/dist/src/utils/api/client.d.ts +0 -15
  115. package/dist/src/utils/api/client.js +0 -27
  116. package/dist/src/utils/approval-errors.d.ts +0 -1
  117. package/dist/src/utils/approval-errors.js +0 -9
  118. package/dist/src/utils/approval-progress.d.ts +0 -12
  119. package/dist/src/utils/approval-progress.js +0 -179
  120. package/dist/src/utils/auth/account-auth.d.ts +0 -33
  121. package/dist/src/utils/auth/account-auth.js +0 -302
  122. package/dist/src/utils/auth/client-credentials.d.ts +0 -26
  123. package/dist/src/utils/auth/client-credentials.js +0 -67
  124. package/dist/src/utils/auth/oauth-callback.d.ts +0 -6
  125. package/dist/src/utils/auth/oauth-callback.js +0 -28
  126. package/dist/src/utils/auth/oauth-errors.d.ts +0 -2
  127. package/dist/src/utils/auth/oauth-errors.js +0 -39
  128. package/dist/src/utils/auth/oauth-flow.d.ts +0 -37
  129. package/dist/src/utils/auth/oauth-flow.js +0 -313
  130. package/dist/src/utils/auth/oauth-transaction.d.ts +0 -35
  131. package/dist/src/utils/auth/oauth-transaction.js +0 -69
  132. package/dist/src/utils/cli-generator-utils.d.ts +0 -14
  133. package/dist/src/utils/cli-generator-utils.js +0 -122
  134. package/dist/src/utils/cli-generator.d.ts +0 -4
  135. package/dist/src/utils/cli-generator.js +0 -831
  136. package/dist/src/utils/cli-options.d.ts +0 -16
  137. package/dist/src/utils/cli-options.js +0 -34
  138. package/dist/src/utils/cli-renderer.d.ts +0 -53
  139. package/dist/src/utils/cli-renderer.js +0 -265
  140. package/dist/src/utils/constants.d.ts +0 -3
  141. package/dist/src/utils/constants.js +0 -5
  142. package/dist/src/utils/directory-detection.d.ts +0 -5
  143. package/dist/src/utils/directory-detection.js +0 -21
  144. package/dist/src/utils/errors.d.ts +0 -33
  145. package/dist/src/utils/errors.js +0 -36
  146. package/dist/src/utils/extensions.d.ts +0 -20
  147. package/dist/src/utils/extensions.js +0 -66
  148. package/dist/src/utils/getCallablePromise.d.ts +0 -6
  149. package/dist/src/utils/getCallablePromise.js +0 -14
  150. package/dist/src/utils/log.d.ts +0 -8
  151. package/dist/src/utils/log.js +0 -26
  152. package/dist/src/utils/manifest-helpers.d.ts +0 -10
  153. package/dist/src/utils/manifest-helpers.js +0 -19
  154. package/dist/src/utils/non-interactive.d.ts +0 -14
  155. package/dist/src/utils/non-interactive.js +0 -15
  156. package/dist/src/utils/package-manager-detector.d.ts +0 -18
  157. package/dist/src/utils/package-manager-detector.js +0 -72
  158. package/dist/src/utils/parameter-resolver.d.ts +0 -179
  159. package/dist/src/utils/parameter-resolver.js +0 -1554
  160. package/dist/src/utils/retry.d.ts +0 -6
  161. package/dist/src/utils/retry.js +0 -21
  162. package/dist/src/utils/schema-formatter.d.ts +0 -11
  163. package/dist/src/utils/schema-formatter.js +0 -131
  164. package/dist/src/utils/serializeAsync.d.ts +0 -2
  165. package/dist/src/utils/serializeAsync.js +0 -16
  166. package/dist/src/utils/spinner.d.ts +0 -1
  167. package/dist/src/utils/spinner.js +0 -21
  168. package/dist/src/utils/string-utils.d.ts +0 -1
  169. package/dist/src/utils/string-utils.js +0 -6
  170. package/dist/src/utils/triggerDrain.d.ts +0 -144
  171. package/dist/src/utils/triggerDrain.js +0 -351
  172. package/dist/src/utils/version-checker.d.ts +0 -17
  173. package/dist/src/utils/version-checker.js +0 -154
  174. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -1,1554 +0,0 @@
1
- import inquirer from "inquirer";
2
- import search from "@inquirer/search";
3
- import chalk from "chalk";
4
- import ora from "ora";
5
- import { z } from "zod";
6
- import { runWithTelemetryContext, } from "@zapier/zapier-sdk";
7
- import { isPositional, buildCapabilityMessage } from "@zapier/zapier-sdk";
8
- import { ZapierCliUserCancellationError, ZapierCliMissingParametersError, ZapierCliValidationError, } from "./errors";
9
- // Zod validation errors are structured objects; flatten them into a
10
- // readable "field: reason" string for CLI error messages.
11
- function formatZodError(error) {
12
- return error.issues
13
- .map((issue) => {
14
- const field = issue.path.length > 0 ? issue.path.join(".") : "input";
15
- return `${field}: ${issue.message}`;
16
- })
17
- .join(", ");
18
- }
19
- // ============================================================================
20
- // Local Resolution Helper Functions
21
- // ============================================================================
22
- /**
23
- * Resolve dependency chain for a parameter using local resolvers
24
- * Returns parameters in the order they need to be resolved
25
- */
26
- function getLocalResolutionOrder(paramName, resolvers, resolved = new Set()) {
27
- const resolver = resolvers[paramName];
28
- if (!resolver || resolver.type === "static" || resolver.type === "constant") {
29
- return [paramName];
30
- }
31
- const order = [];
32
- if ("depends" in resolver && resolver.depends) {
33
- for (const dependency of resolver.depends) {
34
- if (!resolved.has(dependency)) {
35
- order.push(...getLocalResolutionOrder(dependency, resolvers, resolved));
36
- resolved.add(dependency);
37
- }
38
- }
39
- }
40
- if (!resolved.has(paramName)) {
41
- order.push(paramName);
42
- resolved.add(paramName);
43
- }
44
- return order;
45
- }
46
- /**
47
- * Get resolution order for multiple parameters using local resolvers
48
- */
49
- function getLocalResolutionOrderForParams(paramNames, resolvers) {
50
- const resolved = new Set();
51
- const order = [];
52
- for (const paramName of paramNames) {
53
- const paramOrder = getLocalResolutionOrder(paramName, resolvers, resolved);
54
- for (const param of paramOrder) {
55
- if (!order.includes(param)) {
56
- order.push(param);
57
- }
58
- }
59
- }
60
- return order;
61
- }
62
- function unwrapSchema(schema) {
63
- let current = schema;
64
- while (current instanceof z.ZodOptional ||
65
- current instanceof z.ZodDefault ||
66
- current instanceof z.ZodNullable) {
67
- current = current._zod.def.innerType;
68
- }
69
- return current;
70
- }
71
- function coerceToSchemaType(value, schema) {
72
- if (typeof value !== "string")
73
- return value;
74
- const base = unwrapSchema(schema);
75
- if (base instanceof z.ZodNumber) {
76
- const n = Number(value);
77
- return Number.isNaN(n) ? value : n;
78
- }
79
- if (base instanceof z.ZodBoolean) {
80
- if (value === "true")
81
- return true;
82
- if (value === "false")
83
- return false;
84
- }
85
- return value;
86
- }
87
- // ============================================================================
88
- // Schema Parameter Resolver
89
- // ============================================================================
90
- /**
91
- * Append a choice's `hint` to its name as dimmed parens, returning a new
92
- * choice object. An array hint is joined with ", ". Used by every prompt
93
- * backend that renders resolver-supplied choices so the dim styling lives
94
- * in one place.
95
- *
96
- * Default-when-unset: if `hint` is not provided and the choice's `value`
97
- * is a primitive (string or number), the value is auto-rendered as the
98
- * hint. So a resolver returning `{ name: "Slack", value: "SlackCLIAPI" }`
99
- * with no `hint` renders as `Slack (SlackCLIAPI)`. To opt out of the
100
- * auto-default, set `hint: ""` or `hint: []` explicitly.
101
- *
102
- * Skip-when-equal: a hint that matches the rendered name verbatim is
103
- * suppressed so primitive-enum resolvers (where `name === String(value)`)
104
- * don't render "read (read)". Case differences still render both.
105
- */
106
- export function renderChoiceLabel(choice) {
107
- let effectiveHint = choice.hint;
108
- if (effectiveHint === undefined &&
109
- (typeof choice.value === "string" || typeof choice.value === "number")) {
110
- effectiveHint = String(choice.value);
111
- }
112
- if (!effectiveHint ||
113
- (Array.isArray(effectiveHint) && effectiveHint.length === 0)) {
114
- return choice;
115
- }
116
- const hintText = Array.isArray(effectiveHint)
117
- ? effectiveHint.join(", ")
118
- : effectiveHint;
119
- if (hintText === choice.name) {
120
- return choice;
121
- }
122
- return { ...choice, name: `${choice.name} ${chalk.dim(`(${hintText})`)}` };
123
- }
124
- export class SchemaParameterResolver {
125
- constructor() {
126
- this.debug = false;
127
- this.spinner = null;
128
- }
129
- debugLog(message) {
130
- if (this.debug) {
131
- this.stopSpinner();
132
- // stderr so debug output never pollutes stdout (jq, > redirect,
133
- // JSON.parse all consume stdout).
134
- console.error(chalk.gray(`[Zapier CLI] ${message}`));
135
- }
136
- }
137
- startSpinner() {
138
- if (!this.debug && !this.spinner) {
139
- this.spinner = ora({ text: "", spinner: "dots" }).start();
140
- }
141
- }
142
- stopSpinner() {
143
- if (this.spinner) {
144
- this.spinner.stop();
145
- this.spinner = null;
146
- }
147
- }
148
- async resolveParameters(schema, providedParams, sdk, functionName, options) {
149
- return runWithTelemetryContext(async () => {
150
- this.debug = options?.debug ?? false;
151
- const interactiveMode = (options?.interactiveMode ?? true) && !!process.stdin.isTTY;
152
- // 1. Try to parse with current parameters
153
- const parseResult = schema.safeParse(providedParams);
154
- // Get all schema parameters to check which ones have resolvers
155
- const allParams = this.extractParametersFromSchema(schema);
156
- const resolvableParams = allParams.filter((param) => this.hasResolver(param.name, sdk, functionName));
157
- // Get all missing parameters that have resolvers
158
- const missingResolvable = resolvableParams.filter((param) => {
159
- const hasValue = this.getNestedValue(providedParams, param.path) !== undefined;
160
- return !hasValue;
161
- });
162
- // Split missing resolvable params into required vs optional based on
163
- // schema. Optional params still get prompted in interactive mode, but
164
- // in a second pass after the schema-required ones (and their resolver
165
- // dependencies) are settled.
166
- const required = missingResolvable.filter((param) => param.isRequired);
167
- const optional = missingResolvable.filter((param) => !param.isRequired);
168
- if (parseResult.success &&
169
- required.length === 0 &&
170
- optional.length === 0) {
171
- return parseResult.data;
172
- }
173
- if (required.length === 0 && optional.length === 0) {
174
- if (!parseResult.success) {
175
- throw new ZapierCliValidationError(formatZodError(parseResult.error));
176
- }
177
- return parseResult.data;
178
- }
179
- // 2. Resolve required parameters
180
- // Seed resolvedParams with constant resolvers — entries in `resolvers`
181
- // typed `{ type: "constant", value }` for parameters not in the schema.
182
- // Used when a dependent resolver needs a value the user never sets
183
- // (e.g., trigger plugins pin actionType="read" so actionKeyResolver
184
- // and inputsResolver work without actionType being a real input).
185
- const resolverConstants = this.getResolverConstants(sdk, functionName);
186
- const resolvedParams = {
187
- ...resolverConstants,
188
- ...providedParams,
189
- };
190
- const context = {
191
- sdk,
192
- currentParams: providedParams,
193
- resolvedParams,
194
- functionName,
195
- };
196
- const localResolvers = this.getLocalResolvers(sdk, functionName);
197
- if (required.length > 0) {
198
- const requiredParamNames = required.map((p) => p.name);
199
- const requiredResolutionOrder = getLocalResolutionOrderForParams(requiredParamNames, localResolvers);
200
- // Find all parameters that need to be resolved (including dependencies)
201
- const orderedRequiredParams = requiredResolutionOrder
202
- .map((paramName) => {
203
- let param = required.find((p) => p.name === paramName);
204
- if (!param) {
205
- param = optional.find((p) => p.name === paramName);
206
- }
207
- return param;
208
- })
209
- .filter((param) => param !== undefined);
210
- if (!interactiveMode) {
211
- await this.resolveRequiredParamsNonInteractive(orderedRequiredParams, context, resolvedParams, functionName);
212
- }
213
- else {
214
- for (const param of orderedRequiredParams) {
215
- try {
216
- const value = await this.resolveParameter(param, context, functionName, { isOptional: !param.isRequired });
217
- if (param.isRequired || value !== undefined) {
218
- this.setNestedValue(resolvedParams, param.path, value);
219
- context.resolvedParams = resolvedParams;
220
- }
221
- }
222
- catch (error) {
223
- if (this.isUserCancellation(error)) {
224
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
225
- throw new ZapierCliUserCancellationError();
226
- }
227
- throw error;
228
- }
229
- }
230
- }
231
- // Remove resolved dependencies from optional to avoid double-prompting
232
- const resolvedParamNames = new Set(orderedRequiredParams.map((p) => p.name));
233
- optional.splice(0, optional.length, ...optional.filter((p) => !resolvedParamNames.has(p.name)));
234
- }
235
- // 3. Resolve optional parameters individually (each is skippable).
236
- // Skipped in non-interactive mode.
237
- if (interactiveMode && optional.length > 0) {
238
- const optionalParamNames = optional.map((p) => p.name);
239
- const optionalResolutionOrder = getLocalResolutionOrderForParams(optionalParamNames, localResolvers);
240
- const orderedOptionalParams = optionalResolutionOrder
241
- .map((paramName) => optional.find((p) => p.name === paramName))
242
- .filter((param) => param !== undefined);
243
- for (const param of orderedOptionalParams) {
244
- try {
245
- const value = await this.resolveParameter(param, context, functionName, { isOptional: true });
246
- if (value !== undefined) {
247
- this.setNestedValue(resolvedParams, param.path, value);
248
- context.resolvedParams = resolvedParams;
249
- }
250
- }
251
- catch (error) {
252
- if (this.isUserCancellation(error)) {
253
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
254
- throw new ZapierCliUserCancellationError();
255
- }
256
- throw error;
257
- }
258
- }
259
- }
260
- // 4. Validate final parameters
261
- const finalResult = schema.safeParse(resolvedParams);
262
- if (!finalResult.success) {
263
- throw new ZapierCliValidationError(`Parameter validation failed: ${formatZodError(finalResult.error)}`);
264
- }
265
- return finalResult.data;
266
- });
267
- }
268
- extractParametersFromSchema(schema) {
269
- const parameters = [];
270
- // Only handle ZodObject at the top level
271
- if (schema instanceof z.ZodObject) {
272
- const shape = schema.shape;
273
- for (const [key, fieldSchema] of Object.entries(shape)) {
274
- const param = this.analyzeFieldSchema(key, fieldSchema);
275
- if (param) {
276
- parameters.push(param);
277
- }
278
- }
279
- }
280
- return parameters;
281
- }
282
- analyzeFieldSchema(fieldName, fieldSchema) {
283
- let baseSchema = fieldSchema;
284
- let isRequired = true;
285
- // Check if field is optional or has default
286
- if (baseSchema instanceof z.ZodOptional) {
287
- isRequired = false;
288
- baseSchema = baseSchema._zod.def.innerType;
289
- }
290
- if (baseSchema instanceof z.ZodDefault) {
291
- isRequired = false;
292
- baseSchema = baseSchema._zod.def.innerType;
293
- }
294
- return this.createResolvableParameter([fieldName], baseSchema, isRequired);
295
- }
296
- createResolvableParameter(path, schema, isRequired) {
297
- if (path.length === 0)
298
- return null;
299
- const name = path[path.length - 1];
300
- return {
301
- name,
302
- path,
303
- schema,
304
- description: schema.description,
305
- isRequired,
306
- };
307
- }
308
- /**
309
- * Calls `tryResolveWithoutPrompt` on a dynamic resolver.
310
- * Returns the resolution result object, or null if unresolvable / throws.
311
- * Note: { resolvedValue: undefined } is a valid result (e.g. connection when app has no auth);
312
- * only a null return from the resolver itself means "could not auto-resolve".
313
- */
314
- async tryAutoResolve(dynamicResolver, context) {
315
- if (!dynamicResolver.tryResolveWithoutPrompt)
316
- return null;
317
- try {
318
- return await dynamicResolver.tryResolveWithoutPrompt(context.sdk, context.resolvedParams);
319
- }
320
- catch (err) {
321
- console.warn(`Auto-resolver threw unexpectedly; treating as unresolved. Error: ${err instanceof Error ? err.message : String(err)}`);
322
- return null;
323
- }
324
- }
325
- /**
326
- * Non-interactive resolution: auto-resolves what it can via tryAutoResolve,
327
- * throws ZapierCliMissingParametersError for anything that requires user input.
328
- */
329
- async resolveRequiredParamsNonInteractive(params, context, resolvedParams, functionName) {
330
- const missingParams = [];
331
- for (const param of params) {
332
- const resolver = this.getResolver(param.name, context.sdk, functionName);
333
- let autoResolved = null;
334
- if (resolver?.type === "constant") {
335
- autoResolved = {
336
- resolvedValue: resolver.value,
337
- };
338
- }
339
- else if (resolver?.type === "dynamic") {
340
- autoResolved = await this.tryAutoResolve(resolver, context);
341
- }
342
- if (autoResolved != null) {
343
- this.setNestedValue(resolvedParams, param.path, autoResolved.resolvedValue);
344
- context.resolvedParams = resolvedParams;
345
- }
346
- else {
347
- missingParams.push({
348
- name: param.name,
349
- // Required params render as positional CLI args (<name>); so do explicitly positional optional params.
350
- isPositional: param.isRequired || isPositional(param.schema),
351
- });
352
- }
353
- }
354
- if (missingParams.length > 0) {
355
- throw new ZapierCliMissingParametersError(missingParams);
356
- }
357
- }
358
- /**
359
- * Wrap a PromptConfig.validate so internal sentinels (Symbols) bypass
360
- * it. The resolver's validator is intended for actual user values; our
361
- * Skip / Custom / Load-more sentinels are internal control-flow and
362
- * should pass through. Returns `undefined` when the resolver didn't
363
- * supply a validator (so `await search({ ...rest })` doesn't get a
364
- * pass-through identity function).
365
- */
366
- wrapPromptValidate(validate) {
367
- if (!validate)
368
- return undefined;
369
- return (value) => (typeof value === "symbol" ? true : validate(value));
370
- }
371
- /**
372
- * Apply a PromptConfig.filter to a selected value, but only when the
373
- * value is a real data choice (not an internal sentinel). @inquirer/search
374
- * has no built-in filter hook, so the search-backed paths call this
375
- * explicitly before returning.
376
- */
377
- applyPromptFilter(filter, value) {
378
- if (!filter || typeof value === "symbol")
379
- return value;
380
- return filter(value);
381
- }
382
- /**
383
- * If the resolver's PromptConfig sets a `default` value, move the
384
- * matching choice to the front so it's the first selectable item in
385
- * the rendered source. @inquirer/search has no built-in `default`
386
- * option; first-selectable is what Enter picks, so reordering achieves
387
- * the same semantics inquirer.prompt's list had natively.
388
- *
389
- * Returns the original array if no default is set or the default
390
- * doesn't match any current choice.
391
- */
392
- reorderForDefault(matches, defaultValue) {
393
- if (defaultValue === undefined)
394
- return matches;
395
- const idx = matches.findIndex((c) => c.value === defaultValue);
396
- if (idx <= 0)
397
- return matches;
398
- return [matches[idx], ...matches.slice(0, idx), ...matches.slice(idx + 1)];
399
- }
400
- /**
401
- * Build the disabled "if you had X capability, more results would show"
402
- * hints for any unmet capabilities the resolver declared. Returns the
403
- * raw hint strings — callers wrap them into choice objects with whatever
404
- * sentinel value they prefer (disabled choices' values are inert).
405
- */
406
- async computeCapabilityHints(resolver, context) {
407
- if (!resolver.requireCapabilities)
408
- return [];
409
- const capContext = context.sdk.context;
410
- if (!capContext.hasCapability)
411
- return [];
412
- const messages = [];
413
- for (const cap of resolver.requireCapabilities) {
414
- const enabled = await capContext.hasCapability(cap);
415
- if (!enabled)
416
- messages.push(buildCapabilityMessage(cap));
417
- }
418
- return messages;
419
- }
420
- /**
421
- * Unpack a DynamicResolver.fetch result into the three shapes the caller
422
- * cares about: a flat items array, an optional AsyncIterator for further
423
- * pagination, and a hasMore flag. Centralizing keeps the AsyncIterable /
424
- * `{data, nextCursor}` / `TItem[]` discrimination in one place — both
425
- * the main dropdown loop and the search-mode flow consume it.
426
- *
427
- * The function is `async` for the AsyncIterable branch only (we eagerly
428
- * consume the first page so callers don't have to discriminate). The
429
- * other two branches return synchronously; an explicit Promise.resolve
430
- * is unnecessary because async automatically wraps.
431
- *
432
- * Note: callers in search mode intentionally drop `pageIterator` /
433
- * `hasMore` because each search() invocation re-prompts from scratch;
434
- * pagination only matters when the prompt is the dropdown itself.
435
- */
436
- async unpackFetchResult(fetchResult, promptLabel) {
437
- if (fetchResult != null &&
438
- typeof fetchResult === "object" &&
439
- Symbol.asyncIterator in fetchResult) {
440
- const pageIterator = fetchResult[Symbol.asyncIterator]();
441
- const first = await pageIterator.next();
442
- if (!first.done && first.value) {
443
- return {
444
- items: Array.isArray(first.value.data) ? first.value.data : [],
445
- pageIterator,
446
- hasMore: first.value.nextCursor != null,
447
- };
448
- }
449
- return { items: [], pageIterator, hasMore: false };
450
- }
451
- if (fetchResult != null &&
452
- typeof fetchResult === "object" &&
453
- "data" in fetchResult) {
454
- const page = fetchResult;
455
- const hasMore = page.nextCursor != null;
456
- if (hasMore) {
457
- this.debugLog(`Resolver for ${promptLabel} has more pages but no iterator. ` +
458
- `Use toIterable() to enable "Load more..." support.`);
459
- }
460
- return {
461
- items: Array.isArray(page.data) ? page.data : [],
462
- pageIterator: null,
463
- hasMore,
464
- };
465
- }
466
- // Anything else (including null/undefined) is treated as an empty
467
- // list — the cast on a plain array kept downstream code happy, but
468
- // an `Array.isArray` guard is the honest version.
469
- return {
470
- items: Array.isArray(fetchResult) ? fetchResult : [],
471
- pageIterator: null,
472
- hasMore: false,
473
- };
474
- }
475
- /**
476
- * Search-mode dynamic resolver: prompts the user for free-form text, passes
477
- * it to fetch via `search`, and either short-circuits on a primitive return
478
- * (exact match) or renders the results as a search-filterable dropdown.
479
- * Empty results still render the dropdown so the user can fall through to
480
- * "(Use 'foo' as-is)" or "(Try a different search)" rather than being
481
- * stuck.
482
- *
483
- * Known limitation: pagination beyond the first page is dropped. Each
484
- * search() invocation re-prompts from scratch and the user is expected
485
- * to refine their query if too many results came back. If a future
486
- * high-cardinality search resolver needs Load-more here, the
487
- * `pageIterator` / `hasMore` from `unpackFetchResult` is what to wire in.
488
- */
489
- async resolveDynamicWithSearchInput({ resolver, context, promptLabel, isOptional, }) {
490
- // Suffix the placeholder onto the message rather than passing it as
491
- // inquirer's `default`. `inquirer`'s `input` prompt renders `default`
492
- // as prefilled text the user has to delete before typing — so a user
493
- // who just starts typing ends up sending `${placeholder}${typed}` as
494
- // the search term. As a message suffix the hint is read-only.
495
- // Fold "(optional)" and the placeholder hint into one parenthetical
496
- // so an optional search prompt with a hint reads
497
- // "Enter or search app (optional, e.g., 'slack'):" rather than the
498
- // double-paren "Enter or search app (optional) (e.g., 'slack'):".
499
- const parenParts = [];
500
- if (isOptional)
501
- parenParts.push("optional");
502
- if (resolver.placeholder)
503
- parenParts.push(resolver.placeholder);
504
- const parens = parenParts.length > 0 ? ` (${parenParts.join(", ")})` : "";
505
- const message = `Enter or search ${promptLabel}${parens}:`;
506
- // Sentinels are loop-invariant — hoist so they aren't reallocated on
507
- // every re-prompt. Identity comparisons against `selected` still work
508
- // because the same Symbol references are reused.
509
- const SKIP_SENTINEL = Symbol("SKIP");
510
- const USE_AS_IS_SENTINEL = Symbol("USE_AS_IS");
511
- const TRY_AGAIN_SENTINEL = Symbol("TRY_AGAIN");
512
- let lastNote;
513
- while (true) {
514
- this.stopSpinner();
515
- if (lastNote) {
516
- console.log(chalk.yellow(lastNote));
517
- lastNote = undefined;
518
- }
519
- const answers = await inquirer.prompt([
520
- { type: "input", name: "search", message },
521
- ]);
522
- const rawInput = answers.search;
523
- const searchInput = typeof rawInput === "string" ? rawInput.trim() : "";
524
- if (!searchInput) {
525
- if (isOptional)
526
- return undefined;
527
- lastNote = `${promptLabel} is required.`;
528
- continue;
529
- }
530
- // Build params once with `search` injected; pass to both fetch and
531
- // prompt. The public type tells search-mode resolver authors to
532
- // model TParams as `{ search?: string; ...otherDeps }`, so their
533
- // prompt() can rely on seeing the term too.
534
- const searchParams = {
535
- ...context.resolvedParams,
536
- search: searchInput,
537
- };
538
- this.startSpinner();
539
- this.debugLog(`Searching ${promptLabel} for "${searchInput}"`);
540
- let fetchResult;
541
- try {
542
- fetchResult = await resolver.fetch(context.sdk, searchParams);
543
- }
544
- finally {
545
- this.stopSpinner();
546
- }
547
- if (typeof fetchResult === "string" || typeof fetchResult === "number") {
548
- return fetchResult;
549
- }
550
- const { items } = await this.unpackFetchResult(fetchResult, promptLabel);
551
- const choicesConfig = resolver.prompt(items, searchParams);
552
- const dataChoices = (choicesConfig.choices ?? []).map(renderChoiceLabel);
553
- const capabilityHintMessages = await this.computeCapabilityHints(resolver, context);
554
- const selected = await search({
555
- message: choicesConfig.message,
556
- validate: this.wrapPromptValidate(choicesConfig.validate),
557
- // @inquirer/search passes an AbortSignal as the second arg for
558
- // cancelling async sources. All three of our source callbacks are
559
- // pure-local (filter an already-fetched array), so we intentionally
560
- // ignore the signal. A future server-side filter implementation
561
- // would need to honor it.
562
- source: (term) => {
563
- const trimmed = (term ?? "").trim();
564
- const lower = trimmed.toLowerCase();
565
- const matches = trimmed
566
- ? dataChoices.filter((c) => c.name.toLowerCase().includes(lower))
567
- : dataChoices;
568
- // @inquirer/search activates the first selectable item, so
569
- // matches always go first — the user already typed a query at
570
- // the text prompt to get here, and pressing Enter immediately
571
- // should pick the top match rather than (Skip) or one of the
572
- // escape hatches. If the resolver's PromptConfig sets a
573
- // default, surface that match as the first selectable.
574
- const orderedMatches = trimmed
575
- ? matches
576
- : this.reorderForDefault(matches, choicesConfig.default);
577
- const skipChoice = isOptional
578
- ? [{ name: chalk.dim("(Skip)"), value: SKIP_SENTINEL }]
579
- : [];
580
- // JSON.stringify handles the quoting + escape so an input like
581
- // `foo"bar` renders as `(Use "foo\"bar" as-is)` instead of
582
- // breaking the visible label with a stray quote.
583
- const useAsIsChoice = {
584
- name: chalk.dim(`(Use ${JSON.stringify(searchInput)} as-is)`),
585
- value: USE_AS_IS_SENTINEL,
586
- };
587
- const tryAgainChoice = {
588
- name: chalk.dim("(Try a different search)"),
589
- value: TRY_AGAIN_SENTINEL,
590
- };
591
- const out = [];
592
- if (orderedMatches.length === 0) {
593
- // Empty results: the user just typed something and got
594
- // nothing back. Pushing (Skip) first would make pressing
595
- // Enter surprise-skip the parameter they committed to. Bias
596
- // toward "use what I typed" and "try a different search."
597
- out.push(useAsIsChoice);
598
- out.push(tryAgainChoice);
599
- out.push(...skipChoice);
600
- }
601
- else {
602
- out.push(...orderedMatches);
603
- out.push(...skipChoice);
604
- out.push(useAsIsChoice);
605
- out.push(tryAgainChoice);
606
- }
607
- for (const message of capabilityHintMessages) {
608
- out.push({
609
- name: chalk.dim(message),
610
- value: SKIP_SENTINEL,
611
- disabled: true,
612
- });
613
- }
614
- return out;
615
- },
616
- });
617
- if (selected === SKIP_SENTINEL)
618
- return undefined;
619
- // The (Use as-is) escape hatch is a real value heading into the
620
- // same downstream slot as a picked match, so the resolver's
621
- // validate and filter both apply. search()'s wrapped validate
622
- // bypasses sentinels, so we have to invoke validate manually here
623
- // with the typed string. On rejection, surface the validator's
624
- // error as a yellow note above the next text prompt.
625
- if (selected === USE_AS_IS_SENTINEL) {
626
- const validationResult = choicesConfig.validate?.(searchInput);
627
- if (validationResult === false) {
628
- lastNote = `${promptLabel}: invalid value.`;
629
- continue;
630
- }
631
- if (typeof validationResult === "string") {
632
- lastNote = validationResult;
633
- continue;
634
- }
635
- return this.applyPromptFilter(choicesConfig.filter, searchInput);
636
- }
637
- if (selected === TRY_AGAIN_SENTINEL)
638
- continue;
639
- return this.applyPromptFilter(choicesConfig.filter, selected);
640
- }
641
- }
642
- async resolveParameter(param, context, functionName, options) {
643
- const resolver = this.getResolver(param.name, context.sdk, functionName);
644
- if (!resolver) {
645
- throw new Error(`No resolver found for parameter: ${param.name}`);
646
- }
647
- return this.resolveWithResolver(resolver, param, context, {
648
- isOptional: options?.isOptional,
649
- });
650
- }
651
- /**
652
- * Run a resolver to obtain a value for one parameter, prompting the
653
- * user when necessary. Routes to one of several prompt backends and
654
- * has to keep the `PromptConfig` contract honest across each one.
655
- *
656
- * `PromptConfig` field × prompt-backend handling:
657
- *
658
- * | field | list (search()) | checkbox/confirm (inquirer.prompt) | search-mode dropdown (search()) |
659
- * | -------- | --------------- | ---------------------------------- | ------------------------------- |
660
- * | type | required | required | required |
661
- * | name | (set internally)| forwarded | (set internally) |
662
- * | message | forwarded | forwarded | forwarded |
663
- * | choices | filtered in src | passed to inquirer | filtered in src |
664
- * | default | reorder matches | passed (also internal cursor jump) | reorder matches |
665
- * | validate | wrapped | passed (inquirer native) | wrapped + manual for (Use as-is)|
666
- * | filter | manual once | inquirer native (do NOT double) | manual once |
667
- *
668
- * Escape-hatch sentinels (Skip / Custom value / Use as-is / Load more
669
- * / Try again) bypass the resolver's validate/filter because they
670
- * aren't real user values — they're CLI control-flow.
671
- */
672
- async resolveWithResolver(resolver, param, context, options = {}) {
673
- const { arrayIndex, isOptional } = options;
674
- const inArrayContext = arrayIndex != null;
675
- const promptLabel = inArrayContext
676
- ? `${param.name}[${arrayIndex}]`
677
- : param.name;
678
- const promptName = inArrayContext ? "value" : param.name;
679
- this.debugLog(`Resolving ${promptLabel}${isOptional ? " (optional)" : ""}`);
680
- if (resolver.type === "constant") {
681
- const constantResolver = resolver;
682
- this.stopSpinner();
683
- return constantResolver.value;
684
- }
685
- else if (resolver.type === "static") {
686
- const staticResolver = resolver;
687
- const promptConfig = {
688
- type: staticResolver.inputType === "password" ? "password" : "input",
689
- name: promptName,
690
- message: `Enter ${promptLabel}${isOptional ? " (optional)" : ""}:`,
691
- ...(staticResolver.placeholder && {
692
- default: staticResolver.placeholder,
693
- }),
694
- };
695
- this.stopSpinner();
696
- const answers = await inquirer.prompt([promptConfig]);
697
- const value = answers[promptName];
698
- if (isOptional &&
699
- (value === undefined ||
700
- value === "" ||
701
- value === staticResolver.placeholder)) {
702
- return undefined;
703
- }
704
- // Inquirer's "input" prompt always returns a string. Coerce to match
705
- // the schema's underlying type so z.number()/z.boolean() validation
706
- // doesn't reject the value.
707
- return coerceToSchemaType(value, param.schema);
708
- }
709
- else if (resolver.type === "dynamic") {
710
- const dynamicResolver = resolver;
711
- this.startSpinner();
712
- const autoResolution = await this.tryAutoResolve(dynamicResolver, context);
713
- if (autoResolution != null) {
714
- this.stopSpinner();
715
- return autoResolution.resolvedValue;
716
- }
717
- if (dynamicResolver.inputType === "search") {
718
- this.stopSpinner();
719
- return await this.resolveDynamicWithSearchInput({
720
- resolver: dynamicResolver,
721
- context,
722
- promptLabel,
723
- isOptional: isOptional ?? false,
724
- });
725
- }
726
- this.debugLog(`Fetching options for ${promptLabel}`);
727
- let fetchResult;
728
- try {
729
- fetchResult = await dynamicResolver.fetch(context.sdk, context.resolvedParams);
730
- }
731
- finally {
732
- // Mirror the search-mode pattern: stop the spinner whether fetch
733
- // succeeds or throws, so a network/auth error doesn't leave the
734
- // spinner spinning through the error bubbling.
735
- this.stopSpinner();
736
- }
737
- // Primitive returns are only meaningful in `inputType: "search"`
738
- // mode, where they signal an exact-match short-circuit. Without
739
- // search mode there's nothing for the user to disambiguate, so a
740
- // primitive here is a resolver bug. Warn the author loudly, then fall
741
- // back to a manual text prompt so the user isn't stranded mid-command.
742
- if (typeof fetchResult === "string" || typeof fetchResult === "number") {
743
- console.error(chalk.yellow(`Resolver for ${promptLabel} returned a primitive value but is not in search mode. Set inputType: "search" on the resolver if you want exact-match short-circuit. Falling back to manual entry.`));
744
- const fallbackAnswers = await inquirer.prompt([
745
- {
746
- type: "input",
747
- name: promptName,
748
- message: `Enter ${promptLabel}${isOptional ? " (optional)" : ""}:`,
749
- },
750
- ]);
751
- const fallbackValue = fallbackAnswers[promptName];
752
- if (isOptional &&
753
- (fallbackValue === undefined || fallbackValue === "")) {
754
- return undefined;
755
- }
756
- return fallbackValue;
757
- }
758
- const unpacked = await this.unpackFetchResult(fetchResult, promptLabel);
759
- let pageIterator = unpacked.pageIterator;
760
- let items = unpacked.items;
761
- let hasMore = unpacked.hasMore;
762
- const LOAD_MORE_SENTINEL = Symbol("LOAD_MORE");
763
- const SKIP_SENTINEL = Symbol("SKIP");
764
- const CUSTOM_VALUE_SENTINEL = Symbol("CUSTOM_VALUE");
765
- let newItemsStartIndex = -1;
766
- while (true) {
767
- const promptConfig = dynamicResolver.prompt(items, context.resolvedParams);
768
- promptConfig.name = promptName;
769
- // Fold any per-choice `hint` into the rendered name once, here, so
770
- // both the search-backed list path and the checkbox path below
771
- // pick up the dim styling without each having to know about hints.
772
- if (promptConfig.choices) {
773
- promptConfig.choices = promptConfig.choices.map(renderChoiceLabel);
774
- }
775
- // Friendlier than inquirer's default "No selectable choices. All
776
- // choices are disabled." when the resolver returns no items (or
777
- // filters everything out in `prompt`). Use ZapierCliValidationError
778
- // so the message renders to the user; ZapierCliExitError exits
779
- // silently and would be confusing here.
780
- const hasSelectableChoice = promptConfig.choices?.some((c) => !c.disabled);
781
- if (!hasSelectableChoice && !hasMore) {
782
- throw new ZapierCliValidationError(`No ${promptLabel} available to select.`);
783
- }
784
- // Capability hints: shown when no more pages remain and the
785
- // resolver advertised capabilities that would expand results.
786
- // The Skip / (Enter custom value) / (Load more...) sentinels live
787
- // in the list/checkbox branches below since the search-backed list
788
- // path injects them via the source callback rather than mutating
789
- // promptConfig.choices.
790
- const capabilityHints = [];
791
- if (!hasMore) {
792
- const hintMessages = await this.computeCapabilityHints(dynamicResolver, context);
793
- for (const message of hintMessages) {
794
- capabilityHints.push({
795
- name: chalk.dim(message),
796
- value: SKIP_SENTINEL,
797
- disabled: true,
798
- });
799
- }
800
- }
801
- let selected;
802
- if (promptConfig.type === "list") {
803
- // Single-select dropdowns go through @inquirer/search so users
804
- // can type-to-filter long lists. (Load more...) sits as a trailer
805
- // and is reachable whether or not the search yielded matches; the
806
- // user retypes after loading because @inquirer/search can't seed
807
- // its input box.
808
- const dataChoices = promptConfig.choices ?? [];
809
- selected = await search({
810
- message: promptConfig.message,
811
- validate: this.wrapPromptValidate(promptConfig.validate),
812
- source: (term) => {
813
- const trimmed = (term ?? "").trim();
814
- const lower = trimmed.toLowerCase();
815
- const matches = trimmed
816
- ? dataChoices.filter((c) => c.name.toLowerCase().includes(lower))
817
- : dataChoices;
818
- const out = [];
819
- const skipChoice = isOptional
820
- ? [
821
- {
822
- name: chalk.dim("(Skip)"),
823
- value: SKIP_SENTINEL,
824
- },
825
- ]
826
- : [];
827
- const customValueChoice = {
828
- name: chalk.dim("(Enter custom value)"),
829
- value: CUSTOM_VALUE_SENTINEL,
830
- };
831
- // @inquirer/search activates the first selectable item.
832
- // Three cases, with one unifying rule: never put (Skip)
833
- // first when the user is committed (typed a filter,
834
- // default specified) — pressing Enter then surprise-skips.
835
- // - Has matches + (filtered or defaulted) → matches first,
836
- // then Skip and Custom, then Load more as a trailer.
837
- // - No matches + filtered → Custom and Load more above
838
- // Skip, so every constructive option beats out skipping.
839
- // - Otherwise (browsing) → Skip first (the conventional
840
- // optional-dropdown UX), then Custom, matches, Load more.
841
- const orderedMatches = trimmed
842
- ? matches
843
- : this.reorderForDefault(matches, promptConfig.default);
844
- const matchesFirst = trimmed || promptConfig.default !== undefined;
845
- const loadMoreChoice = hasMore && pageIterator
846
- ? {
847
- name: chalk.dim("(Load more...)"),
848
- value: LOAD_MORE_SENTINEL,
849
- }
850
- : null;
851
- if (matchesFirst && orderedMatches.length > 0) {
852
- out.push(...orderedMatches);
853
- out.push(...skipChoice);
854
- out.push(customValueChoice);
855
- if (loadMoreChoice)
856
- out.push(loadMoreChoice);
857
- }
858
- else if (matchesFirst) {
859
- out.push(customValueChoice);
860
- if (loadMoreChoice)
861
- out.push(loadMoreChoice);
862
- out.push(...skipChoice);
863
- }
864
- else {
865
- out.push(...skipChoice);
866
- out.push(customValueChoice);
867
- out.push(...orderedMatches);
868
- if (loadMoreChoice)
869
- out.push(loadMoreChoice);
870
- }
871
- // Capability hints only render when there are actual matches
872
- // (or no search term) so an empty-search view stays focused.
873
- if (capabilityHints.length > 0 &&
874
- (!trimmed || matches.length > 0)) {
875
- out.push(...capabilityHints);
876
- }
877
- return out;
878
- },
879
- });
880
- }
881
- else {
882
- // Multi-select (checkbox) and confirm prompts stay on
883
- // inquirer.prompt — search is single-select.
884
- if (isOptional && promptConfig.choices) {
885
- promptConfig.choices.unshift({
886
- name: chalk.dim("(Skip)"),
887
- value: SKIP_SENTINEL,
888
- });
889
- }
890
- if (hasMore && pageIterator && promptConfig.choices) {
891
- promptConfig.choices.push({
892
- name: chalk.dim("(Load more...)"),
893
- value: LOAD_MORE_SENTINEL,
894
- });
895
- }
896
- if (capabilityHints.length > 0 && promptConfig.choices) {
897
- promptConfig.choices.push(...capabilityHints);
898
- }
899
- // After loading more, jump cursor to first new item.
900
- // Offset by the number of injected choices before the data items (e.g. Skip).
901
- if (newItemsStartIndex >= 0 && promptConfig.choices) {
902
- const injectedBefore = isOptional ? 1 : 0;
903
- const adjustedIndex = newItemsStartIndex + injectedBefore;
904
- if (promptConfig.choices[adjustedIndex]) {
905
- promptConfig.default = promptConfig.choices[adjustedIndex].value;
906
- }
907
- newItemsStartIndex = -1;
908
- }
909
- const answers = await inquirer.prompt([promptConfig]);
910
- selected = answers[promptName];
911
- }
912
- if (selected === SKIP_SENTINEL) {
913
- return undefined;
914
- }
915
- if (selected === CUSTOM_VALUE_SENTINEL) {
916
- const customAnswer = await inquirer.prompt([
917
- {
918
- type: "input",
919
- name: promptName,
920
- message: `Enter ${promptLabel}${isOptional ? " (optional)" : ""}:`,
921
- },
922
- ]);
923
- const value = customAnswer[promptName];
924
- if (isOptional && (value === undefined || value === "")) {
925
- return undefined;
926
- }
927
- return value;
928
- }
929
- const wantsMore = Array.isArray(selected)
930
- ? selected.includes(LOAD_MORE_SENTINEL)
931
- : selected === LOAD_MORE_SENTINEL;
932
- if (wantsMore && pageIterator) {
933
- if (Array.isArray(selected)) {
934
- selected = selected.filter((v) => v !== LOAD_MORE_SENTINEL);
935
- }
936
- const prevLength = items.length;
937
- this.startSpinner();
938
- this.debugLog("Fetching more options...");
939
- const next = await pageIterator.next();
940
- this.stopSpinner();
941
- if (!next.done && next.value) {
942
- items = [...items, ...next.value.data];
943
- hasMore = next.value.nextCursor != null;
944
- newItemsStartIndex = prevLength;
945
- }
946
- else {
947
- hasMore = false;
948
- }
949
- continue;
950
- }
951
- // Only the search-backed list path needs a manual filter call —
952
- // @inquirer/search has no built-in filter hook. inquirer.prompt
953
- // (the checkbox / confirm branch above) already applies the
954
- // resolver's filter natively, so re-applying here would
955
- // double-filter non-idempotent values.
956
- return promptConfig.type === "list"
957
- ? this.applyPromptFilter(promptConfig.filter, selected)
958
- : selected;
959
- }
960
- }
961
- else if (resolver.type === "fields") {
962
- if (isOptional && !inArrayContext) {
963
- this.stopSpinner();
964
- const { confirm } = await inquirer.prompt([
965
- {
966
- type: "confirm",
967
- name: "confirm",
968
- message: `Add ${promptLabel}?`,
969
- default: false,
970
- },
971
- ]);
972
- if (!confirm) {
973
- return undefined;
974
- }
975
- }
976
- return await this.resolveFieldsRecursively(resolver, context, param, { inArrayContext });
977
- }
978
- else if (resolver.type === "array") {
979
- return await this.resolveArrayRecursively(resolver, context, param);
980
- }
981
- throw new Error(`Unknown resolver type for ${promptLabel}`);
982
- }
983
- async resolveFieldsRecursively(resolver, context, param, options = {}) {
984
- const inputs = {};
985
- let processedFieldKeys = new Set();
986
- let iteration = 0;
987
- const maxIterations = 10; // Prevent infinite loops
988
- while (iteration < maxIterations) {
989
- iteration++;
990
- // Update context with current inputs so they're passed to listInputFields
991
- const updatedContext = {
992
- ...context,
993
- resolvedParams: {
994
- ...context.resolvedParams,
995
- inputs,
996
- },
997
- };
998
- this.debugLog(`Fetching input fields for ${param.name}${iteration > 1 ? ` (iteration ${iteration})` : ""}`);
999
- this.startSpinner();
1000
- const rootFieldItems = await resolver.fetch(updatedContext.sdk, updatedContext.resolvedParams);
1001
- this.stopSpinner();
1002
- if (!rootFieldItems || rootFieldItems.length === 0) {
1003
- if (iteration === 1) {
1004
- console.log(chalk.yellow(`No input fields required for this action.`));
1005
- }
1006
- break;
1007
- }
1008
- // Process fields recursively, maintaining fieldset structure
1009
- const fieldStats = await this.processFieldItems(rootFieldItems, inputs, processedFieldKeys, [], iteration, updatedContext, { inArrayContext: options.inArrayContext });
1010
- // If no new fields were processed, we're done
1011
- if (fieldStats.newRequired === 0 && fieldStats.newOptional === 0) {
1012
- break;
1013
- }
1014
- // If we only processed optional fields and skipped them, no need to re-fetch
1015
- if (fieldStats.newRequired === 0 && fieldStats.optionalSkipped) {
1016
- break;
1017
- }
1018
- }
1019
- if (iteration >= maxIterations) {
1020
- console.log(chalk.yellow(`\n⚠️ Maximum field resolution iterations reached. Some dynamic fields may not have been discovered.`));
1021
- }
1022
- // Apply transform if provided
1023
- if (resolver.transform) {
1024
- return resolver.transform(inputs);
1025
- }
1026
- return inputs;
1027
- }
1028
- /**
1029
- * Resolves an array parameter by repeatedly prompting for items until user says no
1030
- */
1031
- async resolveArrayRecursively(resolver, context, param) {
1032
- const items = [];
1033
- const minItems = resolver.minItems ?? 0;
1034
- const maxItems = resolver.maxItems ?? Infinity;
1035
- while (items.length < maxItems) {
1036
- const currentIndex = items.length;
1037
- // Skip confirmation if we haven't hit minItems yet - just collect the item
1038
- if (currentIndex >= minItems) {
1039
- this.stopSpinner();
1040
- const confirmAnswer = await inquirer.prompt([
1041
- {
1042
- type: "confirm",
1043
- name: "addItem",
1044
- message: `Add ${param.name}[${currentIndex}]?`,
1045
- default: false,
1046
- },
1047
- ]);
1048
- if (!confirmAnswer.addItem) {
1049
- break;
1050
- }
1051
- }
1052
- // Fetch the inner resolver for this item
1053
- const innerResolver = await resolver.fetch(context.sdk, context.resolvedParams);
1054
- const itemValue = await this.resolveWithResolver(innerResolver, param, context, { arrayIndex: currentIndex });
1055
- items.push(itemValue);
1056
- // Update context with current array for subsequent iterations
1057
- context.resolvedParams = {
1058
- ...context.resolvedParams,
1059
- [param.name]: items,
1060
- };
1061
- }
1062
- if (items.length >= maxItems) {
1063
- console.log(chalk.gray(`Maximum of ${maxItems} items reached.`));
1064
- }
1065
- return items;
1066
- }
1067
- /**
1068
- * Recursively processes fieldsets and their fields, maintaining natural structure
1069
- * and creating nested inputs as needed (e.g., fieldset "foo" becomes inputs.foo = [{}])
1070
- */
1071
- async processFieldItems(items, targetInputs, processedFieldKeys, fieldsetPath = [], iteration = 1, context, options = {}) {
1072
- let newRequiredCount = 0;
1073
- let newOptionalCount = 0;
1074
- let optionalSkipped = false;
1075
- for (const item of items) {
1076
- const typedItem = item;
1077
- if (typedItem.type === "fieldset" && typedItem.fields && typedItem.key) {
1078
- // Show fieldset context to user
1079
- const fieldsetTitle = typedItem.title || typedItem.key;
1080
- const pathDisplay = fieldsetPath.length > 0 ? ` (in ${fieldsetPath.join(" > ")})` : "";
1081
- console.log(chalk.cyan(`\n📁 Processing fieldset: ${fieldsetTitle}${pathDisplay}`));
1082
- // Create fieldset array in target inputs if it doesn't exist
1083
- if (!targetInputs[typedItem.key]) {
1084
- targetInputs[typedItem.key] = [{}];
1085
- }
1086
- // Process fields within this fieldset recursively
1087
- const fieldsetTarget = targetInputs[typedItem.key][0];
1088
- const nestedPath = [...fieldsetPath, fieldsetTitle];
1089
- const nestedStats = await this.processFieldItems(typedItem.fields, fieldsetTarget, processedFieldKeys, nestedPath, iteration, context, options);
1090
- newRequiredCount += nestedStats.newRequired;
1091
- newOptionalCount += nestedStats.newOptional;
1092
- if (nestedStats.optionalSkipped) {
1093
- optionalSkipped = true;
1094
- }
1095
- }
1096
- else if (typedItem.type === "input_field" && typedItem.key) {
1097
- // Skip if already processed
1098
- if (processedFieldKeys.has(typedItem.key)) {
1099
- continue;
1100
- }
1101
- const isRequired = typedItem.is_required || false;
1102
- if (isRequired) {
1103
- // Process required field immediately
1104
- newRequiredCount++;
1105
- if (typedItem.resolver && context) {
1106
- const param = {
1107
- name: typedItem.key,
1108
- path: [typedItem.key],
1109
- schema: z.unknown(),
1110
- isRequired: true,
1111
- };
1112
- targetInputs[typedItem.key] = await this.resolveWithResolver(typedItem.resolver, param, context, { isOptional: false });
1113
- }
1114
- else {
1115
- await this.promptForField(typedItem, targetInputs, context);
1116
- }
1117
- processedFieldKeys.add(typedItem.key);
1118
- }
1119
- else {
1120
- // Collect optional fields for batch processing
1121
- newOptionalCount++;
1122
- }
1123
- }
1124
- // Skip info fields - they're for display only
1125
- }
1126
- // Handle optional fields after processing all required fields
1127
- if (newOptionalCount > 0) {
1128
- const optionalFields = items.filter((item) => {
1129
- const typedItem = item;
1130
- return (typedItem.type === "input_field" &&
1131
- typedItem.key &&
1132
- !typedItem.is_required &&
1133
- !processedFieldKeys.has(typedItem.key));
1134
- });
1135
- if (optionalFields.length > 0) {
1136
- const pathContext = fieldsetPath.length > 0 ? ` in ${fieldsetPath.join(" > ")}` : "";
1137
- // In array context, prompt for all fields directly without confirmation
1138
- if (options.inArrayContext) {
1139
- for (const field of optionalFields) {
1140
- await this.promptForField(field, targetInputs, context);
1141
- const typedField = field;
1142
- processedFieldKeys.add(typedField.key);
1143
- }
1144
- }
1145
- else {
1146
- console.log(chalk.gray(`\nThere are ${optionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available${pathContext}.`));
1147
- try {
1148
- const shouldConfigureOptional = await inquirer.prompt([
1149
- {
1150
- type: "confirm",
1151
- name: "configure",
1152
- message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields${pathContext}?`,
1153
- default: false,
1154
- },
1155
- ]);
1156
- if (shouldConfigureOptional.configure) {
1157
- console.log(chalk.cyan(`\nOptional fields${pathContext}:`));
1158
- for (const field of optionalFields) {
1159
- await this.promptForField(field, targetInputs, context);
1160
- const typedField = field;
1161
- processedFieldKeys.add(typedField.key);
1162
- }
1163
- }
1164
- else {
1165
- optionalSkipped = true;
1166
- // Mark these fields as processed even if skipped to avoid re-asking
1167
- optionalFields.forEach((field) => {
1168
- const typedField = field;
1169
- processedFieldKeys.add(typedField.key);
1170
- });
1171
- }
1172
- }
1173
- catch (error) {
1174
- if (this.isUserCancellation(error)) {
1175
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
1176
- throw new ZapierCliUserCancellationError();
1177
- }
1178
- throw error;
1179
- }
1180
- }
1181
- }
1182
- }
1183
- return {
1184
- newRequired: newRequiredCount,
1185
- newOptional: newOptionalCount,
1186
- optionalSkipped,
1187
- };
1188
- }
1189
- getNestedValue(obj, path) {
1190
- return path.reduce((current, key) => current?.[key], obj);
1191
- }
1192
- setNestedValue(obj, path, value) {
1193
- const lastKey = path[path.length - 1];
1194
- const parent = path.slice(0, -1).reduce((current, key) => {
1195
- const currentObj = current;
1196
- if (!(key in currentObj)) {
1197
- currentObj[key] = {};
1198
- }
1199
- return currentObj[key];
1200
- }, obj);
1201
- parent[lastKey] = value;
1202
- }
1203
- /**
1204
- * Extract and normalize field metadata from raw field object
1205
- */
1206
- extractFieldMetadata(field) {
1207
- const fieldObj = field;
1208
- const valueType = fieldObj.value_type || "string";
1209
- return {
1210
- key: fieldObj.key,
1211
- title: fieldObj.title || fieldObj.label || fieldObj.key,
1212
- description: fieldObj.description || fieldObj.helpText,
1213
- isRequired: fieldObj.is_required || false,
1214
- defaultValue: fieldObj.default_value ?? fieldObj.default,
1215
- valueType,
1216
- hasDropdown: fieldObj.format === "SELECT" || Boolean(fieldObj.choices),
1217
- isMultiSelect: Boolean(valueType === "array" ||
1218
- (fieldObj.items && fieldObj.items.type !== undefined)),
1219
- inlineChoices: fieldObj.choices,
1220
- };
1221
- }
1222
- /**
1223
- * Fetch a page of choices for a dropdown field
1224
- */
1225
- async fetchChoices(fieldMeta, inputs, context, cursor) {
1226
- try {
1227
- this.debugLog(cursor
1228
- ? `Fetching more choices for ${fieldMeta.title}`
1229
- : `Fetching choices for ${fieldMeta.title}`);
1230
- this.startSpinner();
1231
- const page = await context.sdk.listActionInputFieldChoices({
1232
- app: context.resolvedParams.app,
1233
- action: context.resolvedParams.action,
1234
- actionType: context.resolvedParams.actionType,
1235
- connection: context.resolvedParams.connection,
1236
- inputField: fieldMeta.key,
1237
- inputs,
1238
- ...(cursor && { cursor }),
1239
- });
1240
- this.stopSpinner();
1241
- const choices = page.data.map((choice) => ({
1242
- label: choice.label || choice.key || String(choice.value),
1243
- value: choice.value ?? choice.key,
1244
- }));
1245
- if (choices.length === 0 && !cursor) {
1246
- console.log(chalk.yellow(`No choices available for ${fieldMeta.title}`));
1247
- }
1248
- return {
1249
- choices,
1250
- nextCursor: page.nextCursor,
1251
- };
1252
- }
1253
- catch (error) {
1254
- this.stopSpinner();
1255
- console.warn(chalk.yellow(`Failed to fetch choices for ${fieldMeta.title}:`), error);
1256
- return { choices: [] };
1257
- }
1258
- }
1259
- /**
1260
- * Prompt user with choices (handles both single and multi-select with pagination).
1261
- * Single-select goes through @inquirer/search so users can type-to-filter long
1262
- * dropdowns (SELECT fields); multi-select stays on inquirer.prompt since search
1263
- * is single-select only.
1264
- */
1265
- async promptWithChoices({ fieldMeta, choices: initialChoices, nextCursor: initialCursor, inputs, context, }) {
1266
- this.stopSpinner();
1267
- const choices = [...initialChoices];
1268
- let nextCursor = initialCursor;
1269
- const LOAD_MORE_SENTINEL = Symbol("LOAD_MORE");
1270
- const SKIP_SENTINEL = Symbol("SKIP");
1271
- const CUSTOM_VALUE_SENTINEL = Symbol("CUSTOM_VALUE");
1272
- const message = `${fieldMeta.title}${fieldMeta.isRequired ? " (required)" : " (optional)"}:`;
1273
- while (true) {
1274
- let selectedValue;
1275
- if (!fieldMeta.isMultiSelect) {
1276
- const dataChoices = choices.map((c) => ({
1277
- name: c.label,
1278
- value: c.value,
1279
- }));
1280
- selectedValue = await search({
1281
- message,
1282
- source: (term) => {
1283
- const trimmed = (term ?? "").trim();
1284
- const lower = trimmed.toLowerCase();
1285
- const matches = trimmed
1286
- ? dataChoices.filter((c) => c.name.toLowerCase().includes(lower))
1287
- : dataChoices;
1288
- const out = [];
1289
- const skipChoice = !fieldMeta.isRequired
1290
- ? [{ name: chalk.dim("(Skip)"), value: SKIP_SENTINEL }]
1291
- : [];
1292
- // Dynamic dropdowns don't always return every valid value, so a
1293
- // custom-value escape hatch is essential for SELECT fields.
1294
- const customValueChoice = {
1295
- name: chalk.dim("(Enter custom value)"),
1296
- value: CUSTOM_VALUE_SENTINEL,
1297
- };
1298
- // Same ordering rule as the main dynamic-list source: never
1299
- // let (Skip) be the first selectable when the user is
1300
- // committed (filtered). Empty filtered results put Custom
1301
- // and Load more above Skip so every constructive option
1302
- // beats out skipping. Otherwise matches go first.
1303
- const loadMoreChoice = nextCursor && context
1304
- ? {
1305
- name: chalk.dim("(Load more...)"),
1306
- value: LOAD_MORE_SENTINEL,
1307
- }
1308
- : null;
1309
- if (trimmed && matches.length > 0) {
1310
- out.push(...matches);
1311
- out.push(...skipChoice);
1312
- out.push(customValueChoice);
1313
- if (loadMoreChoice)
1314
- out.push(loadMoreChoice);
1315
- }
1316
- else if (trimmed) {
1317
- out.push(customValueChoice);
1318
- if (loadMoreChoice)
1319
- out.push(loadMoreChoice);
1320
- out.push(...skipChoice);
1321
- }
1322
- else {
1323
- out.push(...skipChoice);
1324
- out.push(customValueChoice);
1325
- out.push(...matches);
1326
- if (loadMoreChoice)
1327
- out.push(loadMoreChoice);
1328
- }
1329
- return out;
1330
- },
1331
- });
1332
- if (selectedValue === SKIP_SENTINEL) {
1333
- return undefined;
1334
- }
1335
- if (selectedValue === CUSTOM_VALUE_SENTINEL) {
1336
- return await this.promptFreeForm(fieldMeta);
1337
- }
1338
- }
1339
- else {
1340
- const promptChoices = choices.map((c) => ({
1341
- name: c.label,
1342
- value: c.value,
1343
- }));
1344
- if (nextCursor) {
1345
- promptChoices.push({
1346
- name: chalk.dim("(Load more...)"),
1347
- value: LOAD_MORE_SENTINEL,
1348
- });
1349
- }
1350
- const promptConfig = {
1351
- type: "checkbox",
1352
- name: fieldMeta.key,
1353
- message,
1354
- choices: promptChoices,
1355
- validate: (input) => {
1356
- if (fieldMeta.isRequired && (!input || input.length === 0)) {
1357
- return "At least one selection is required";
1358
- }
1359
- return true;
1360
- },
1361
- };
1362
- const answer = await inquirer.prompt([promptConfig]);
1363
- selectedValue = answer[fieldMeta.key];
1364
- }
1365
- const wantsMore = fieldMeta.isMultiSelect
1366
- ? Array.isArray(selectedValue) &&
1367
- selectedValue.includes(LOAD_MORE_SENTINEL)
1368
- : selectedValue === LOAD_MORE_SENTINEL;
1369
- if (wantsMore && nextCursor && context) {
1370
- if (fieldMeta.isMultiSelect && Array.isArray(selectedValue)) {
1371
- selectedValue = selectedValue.filter((v) => v !== LOAD_MORE_SENTINEL);
1372
- }
1373
- const result = await this.fetchChoices(fieldMeta, inputs, context, nextCursor);
1374
- choices.push(...result.choices);
1375
- nextCursor = result.nextCursor;
1376
- continue;
1377
- }
1378
- return selectedValue;
1379
- }
1380
- }
1381
- /**
1382
- * Prompt user for free-form input (text or boolean)
1383
- */
1384
- async promptFreeForm(fieldMeta) {
1385
- const promptConfig = {
1386
- name: fieldMeta.key,
1387
- message: `${fieldMeta.title}${fieldMeta.isRequired ? " (required)" : " (optional)"}:`,
1388
- };
1389
- if (fieldMeta.valueType === "boolean") {
1390
- promptConfig.type = "confirm";
1391
- promptConfig.default =
1392
- fieldMeta.defaultValue !== undefined
1393
- ? Boolean(fieldMeta.defaultValue)
1394
- : undefined;
1395
- }
1396
- else if (fieldMeta.valueType === "array") {
1397
- promptConfig.type = "input";
1398
- promptConfig.default = fieldMeta.defaultValue;
1399
- promptConfig.message = `${fieldMeta.title}${fieldMeta.isRequired ? " (required)" : " (optional)"} (JSON array or comma-separated):`;
1400
- promptConfig.validate = (input) => {
1401
- if (fieldMeta.isRequired && !input) {
1402
- return "This field is required";
1403
- }
1404
- return true;
1405
- };
1406
- promptConfig.filter = (input) => {
1407
- if (!input)
1408
- return input;
1409
- const trimmed = input.trim();
1410
- if (trimmed.startsWith("[")) {
1411
- try {
1412
- return JSON.parse(trimmed);
1413
- }
1414
- catch {
1415
- // Fall through to comma-separated
1416
- }
1417
- }
1418
- return trimmed.split(",").map((s) => s.trim());
1419
- };
1420
- }
1421
- else {
1422
- promptConfig.type = "input";
1423
- promptConfig.default = fieldMeta.defaultValue;
1424
- promptConfig.validate = (input) => {
1425
- if (fieldMeta.isRequired && !input) {
1426
- return "This field is required";
1427
- }
1428
- return true;
1429
- };
1430
- }
1431
- // Add help text if available
1432
- if (fieldMeta.description) {
1433
- promptConfig.prefix = chalk.gray(`ℹ ${fieldMeta.description}\n`);
1434
- }
1435
- try {
1436
- const answer = await inquirer.prompt([promptConfig]);
1437
- return answer[fieldMeta.key];
1438
- }
1439
- catch (error) {
1440
- if (this.isUserCancellation(error)) {
1441
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
1442
- throw new ZapierCliUserCancellationError();
1443
- }
1444
- throw error;
1445
- }
1446
- }
1447
- /**
1448
- * Store field value in inputs object with validation
1449
- */
1450
- storeFieldValue(inputs, key, value, isRequired) {
1451
- try {
1452
- if (value !== undefined && value !== "") {
1453
- inputs[key] = value;
1454
- }
1455
- else if (isRequired) {
1456
- throw new Error(`Required field ${key} cannot be empty`);
1457
- }
1458
- }
1459
- catch (error) {
1460
- if (this.isUserCancellation(error)) {
1461
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
1462
- throw new ZapierCliUserCancellationError();
1463
- }
1464
- throw error;
1465
- }
1466
- }
1467
- async promptForField(field, inputs, context) {
1468
- const fieldMeta = this.extractFieldMetadata(field);
1469
- // Get choices - either inline or fetched from API
1470
- let choices = [];
1471
- let nextCursor;
1472
- if (fieldMeta.inlineChoices) {
1473
- // Use inline choices directly
1474
- choices = fieldMeta.inlineChoices;
1475
- }
1476
- else if (fieldMeta.hasDropdown && context) {
1477
- // Fetch choices from API
1478
- const result = await this.fetchChoices(fieldMeta, inputs, context);
1479
- choices = result.choices;
1480
- nextCursor = result.nextCursor;
1481
- }
1482
- // Prompt user based on field type
1483
- let selectedValue;
1484
- if (choices.length > 0) {
1485
- selectedValue = await this.promptWithChoices({
1486
- fieldMeta,
1487
- choices,
1488
- nextCursor,
1489
- inputs,
1490
- context,
1491
- });
1492
- }
1493
- else {
1494
- selectedValue = await this.promptFreeForm(fieldMeta);
1495
- }
1496
- // Store result
1497
- this.storeFieldValue(inputs, fieldMeta.key, selectedValue, fieldMeta.isRequired);
1498
- }
1499
- isUserCancellation(error) {
1500
- const errorObj = error;
1501
- return (errorObj?.name === "ExitPromptError" ||
1502
- errorObj?.message?.includes("User force closed") ||
1503
- errorObj?.isTTYError === true);
1504
- }
1505
- hasResolver(paramName, sdk, functionName) {
1506
- // Check plugin-specific resolvers first
1507
- if (functionName && typeof sdk.getRegistry === "function") {
1508
- const registry = sdk.getRegistry({ package: "cli" });
1509
- const functionInfo = registry.functions.find((f) => f.name === functionName);
1510
- if (functionInfo && functionInfo.resolvers?.[paramName]) {
1511
- return true;
1512
- }
1513
- }
1514
- // No global registry fallback
1515
- return false;
1516
- }
1517
- getResolver(paramName, sdk, functionName) {
1518
- // Check plugin-specific resolvers first
1519
- if (functionName && typeof sdk.getRegistry === "function") {
1520
- const registry = sdk.getRegistry({ package: "cli" });
1521
- const functionInfo = registry.functions.find((f) => f.name === functionName);
1522
- if (functionInfo && functionInfo.resolvers?.[paramName]) {
1523
- return functionInfo.resolvers[paramName];
1524
- }
1525
- }
1526
- // No global registry fallback
1527
- return null;
1528
- }
1529
- getLocalResolvers(sdk, functionName) {
1530
- if (!functionName || typeof sdk.getRegistry !== "function") {
1531
- return {};
1532
- }
1533
- const registry = sdk.getRegistry();
1534
- const functionInfo = registry.functions.find((f) => f.name === functionName);
1535
- return functionInfo?.resolvers || {};
1536
- }
1537
- getResolverConstants(sdk, functionName) {
1538
- if (!functionName || typeof sdk.getRegistry !== "function") {
1539
- return {};
1540
- }
1541
- const registry = sdk.getRegistry();
1542
- const functionInfo = registry.functions.find((f) => f.name === functionName);
1543
- const resolvers = functionInfo?.resolvers ?? {};
1544
- const constants = {};
1545
- for (const [key, resolver] of Object.entries(resolvers)) {
1546
- if (resolver &&
1547
- typeof resolver === "object" &&
1548
- resolver.type === "constant") {
1549
- constants[key] = resolver.value;
1550
- }
1551
- }
1552
- return constants;
1553
- }
1554
- }