@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.
- package/CHANGELOG.md +38 -0
- package/dist/cli.cjs +51 -36
- package/dist/cli.mjs +53 -38
- package/dist/experimental.cjs +11 -3
- package/dist/experimental.mjs +12 -4
- package/dist/index.cjs +12 -4
- package/dist/index.mjs +13 -5
- package/package.json +3 -3
- package/dist/package.json +0 -121
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.js +0 -219
- package/dist/src/experimental.d.ts +0 -33
- package/dist/src/experimental.js +0 -86
- package/dist/src/generators/ast-generator.d.ts +0 -41
- package/dist/src/generators/ast-generator.js +0 -405
- package/dist/src/index.d.ts +0 -5
- package/dist/src/index.js +0 -5
- package/dist/src/login/config.d.ts +0 -4
- package/dist/src/login/config.js +0 -21
- package/dist/src/login/credentials-revoke.d.ts +0 -14
- package/dist/src/login/credentials-revoke.js +0 -95
- package/dist/src/login/credentials-store.d.ts +0 -60
- package/dist/src/login/credentials-store.js +0 -181
- package/dist/src/login/filesystem-cache.d.ts +0 -25
- package/dist/src/login/filesystem-cache.js +0 -195
- package/dist/src/login/index.d.ts +0 -110
- package/dist/src/login/index.js +0 -424
- package/dist/src/login/keychain.d.ts +0 -18
- package/dist/src/login/keychain.js +0 -74
- package/dist/src/login/legacy-jwt.d.ts +0 -4
- package/dist/src/login/legacy-jwt.js +0 -18
- package/dist/src/login.d.ts +0 -10
- package/dist/src/login.js +0 -10
- package/dist/src/paths.d.ts +0 -1
- package/dist/src/paths.js +0 -6
- package/dist/src/plugins/add/index.d.ts +0 -254
- package/dist/src/plugins/add/index.js +0 -95
- package/dist/src/plugins/add/schemas.d.ts +0 -8
- package/dist/src/plugins/add/schemas.js +0 -22
- package/dist/src/plugins/auth/credentials-base-url.d.ts +0 -11
- package/dist/src/plugins/auth/credentials-base-url.js +0 -24
- package/dist/src/plugins/buildManifest/index.d.ts +0 -120
- package/dist/src/plugins/buildManifest/index.js +0 -71
- package/dist/src/plugins/buildManifest/schemas.d.ts +0 -44
- package/dist/src/plugins/buildManifest/schemas.js +0 -17
- package/dist/src/plugins/bundleCode/index.d.ts +0 -23
- package/dist/src/plugins/bundleCode/index.js +0 -62
- package/dist/src/plugins/bundleCode/schemas.d.ts +0 -10
- package/dist/src/plugins/bundleCode/schemas.js +0 -19
- package/dist/src/plugins/cliOverrides/index.d.ts +0 -21
- package/dist/src/plugins/cliOverrides/index.js +0 -17
- package/dist/src/plugins/curl/index.d.ts +0 -76
- package/dist/src/plugins/curl/index.js +0 -236
- package/dist/src/plugins/curl/schemas.d.ts +0 -40
- package/dist/src/plugins/curl/schemas.js +0 -108
- package/dist/src/plugins/curl/utils.d.ts +0 -34
- package/dist/src/plugins/curl/utils.js +0 -150
- package/dist/src/plugins/drainTriggerInbox/index.d.ts +0 -46
- package/dist/src/plugins/drainTriggerInbox/index.js +0 -178
- package/dist/src/plugins/feedback/index.d.ts +0 -24
- package/dist/src/plugins/feedback/index.js +0 -52
- package/dist/src/plugins/feedback/schemas.d.ts +0 -5
- package/dist/src/plugins/feedback/schemas.js +0 -8
- package/dist/src/plugins/generateAppTypes/index.d.ts +0 -265
- package/dist/src/plugins/generateAppTypes/index.js +0 -129
- package/dist/src/plugins/generateAppTypes/schemas.d.ts +0 -58
- package/dist/src/plugins/generateAppTypes/schemas.js +0 -21
- package/dist/src/plugins/getLoginConfigPath/index.d.ts +0 -16
- package/dist/src/plugins/getLoginConfigPath/index.js +0 -9
- package/dist/src/plugins/getLoginConfigPath/schemas.d.ts +0 -3
- package/dist/src/plugins/getLoginConfigPath/schemas.js +0 -5
- package/dist/src/plugins/index.d.ts +0 -15
- package/dist/src/plugins/index.js +0 -15
- package/dist/src/plugins/init/display.d.ts +0 -9
- package/dist/src/plugins/init/display.js +0 -72
- package/dist/src/plugins/init/index.d.ts +0 -21
- package/dist/src/plugins/init/index.js +0 -56
- package/dist/src/plugins/init/schemas.d.ts +0 -9
- package/dist/src/plugins/init/schemas.js +0 -19
- package/dist/src/plugins/init/steps.d.ts +0 -39
- package/dist/src/plugins/init/steps.js +0 -141
- package/dist/src/plugins/init/types.d.ts +0 -31
- package/dist/src/plugins/init/types.js +0 -1
- package/dist/src/plugins/init/utils.d.ts +0 -48
- package/dist/src/plugins/init/utils.js +0 -135
- package/dist/src/plugins/login/index.d.ts +0 -25
- package/dist/src/plugins/login/index.js +0 -12
- package/dist/src/plugins/login/schemas.d.ts +0 -9
- package/dist/src/plugins/login/schemas.js +0 -27
- package/dist/src/plugins/logout/index.d.ts +0 -30
- package/dist/src/plugins/logout/index.js +0 -33
- package/dist/src/plugins/logout/schemas.d.ts +0 -2
- package/dist/src/plugins/logout/schemas.js +0 -4
- package/dist/src/plugins/mcp/index.d.ts +0 -28
- package/dist/src/plugins/mcp/index.js +0 -24
- package/dist/src/plugins/mcp/schemas.d.ts +0 -5
- package/dist/src/plugins/mcp/schemas.js +0 -10
- package/dist/src/plugins/signup/index.d.ts +0 -25
- package/dist/src/plugins/signup/index.js +0 -12
- package/dist/src/plugins/signup/schemas.d.ts +0 -9
- package/dist/src/plugins/signup/schemas.js +0 -26
- package/dist/src/plugins/signup/test-harness.d.ts +0 -34
- package/dist/src/plugins/signup/test-harness.js +0 -76
- package/dist/src/plugins/watchTriggerInbox/index.d.ts +0 -46
- package/dist/src/plugins/watchTriggerInbox/index.js +0 -166
- package/dist/src/sdk.d.ts +0 -17
- package/dist/src/sdk.js +0 -67
- package/dist/src/telemetry/builders.d.ts +0 -44
- package/dist/src/telemetry/builders.js +0 -59
- package/dist/src/telemetry/events.d.ts +0 -40
- package/dist/src/telemetry/events.js +0 -4
- package/dist/src/types/sdk.d.ts +0 -6
- package/dist/src/types/sdk.js +0 -1
- package/dist/src/utils/api/client.d.ts +0 -15
- package/dist/src/utils/api/client.js +0 -27
- package/dist/src/utils/approval-errors.d.ts +0 -1
- package/dist/src/utils/approval-errors.js +0 -9
- package/dist/src/utils/approval-progress.d.ts +0 -12
- package/dist/src/utils/approval-progress.js +0 -179
- package/dist/src/utils/auth/account-auth.d.ts +0 -33
- package/dist/src/utils/auth/account-auth.js +0 -302
- package/dist/src/utils/auth/client-credentials.d.ts +0 -26
- package/dist/src/utils/auth/client-credentials.js +0 -67
- package/dist/src/utils/auth/oauth-callback.d.ts +0 -6
- package/dist/src/utils/auth/oauth-callback.js +0 -28
- package/dist/src/utils/auth/oauth-errors.d.ts +0 -2
- package/dist/src/utils/auth/oauth-errors.js +0 -39
- package/dist/src/utils/auth/oauth-flow.d.ts +0 -37
- package/dist/src/utils/auth/oauth-flow.js +0 -313
- package/dist/src/utils/auth/oauth-transaction.d.ts +0 -35
- package/dist/src/utils/auth/oauth-transaction.js +0 -69
- package/dist/src/utils/cli-generator-utils.d.ts +0 -14
- package/dist/src/utils/cli-generator-utils.js +0 -122
- package/dist/src/utils/cli-generator.d.ts +0 -4
- package/dist/src/utils/cli-generator.js +0 -831
- package/dist/src/utils/cli-options.d.ts +0 -16
- package/dist/src/utils/cli-options.js +0 -34
- package/dist/src/utils/cli-renderer.d.ts +0 -53
- package/dist/src/utils/cli-renderer.js +0 -265
- package/dist/src/utils/constants.d.ts +0 -3
- package/dist/src/utils/constants.js +0 -5
- package/dist/src/utils/directory-detection.d.ts +0 -5
- package/dist/src/utils/directory-detection.js +0 -21
- package/dist/src/utils/errors.d.ts +0 -33
- package/dist/src/utils/errors.js +0 -36
- package/dist/src/utils/extensions.d.ts +0 -20
- package/dist/src/utils/extensions.js +0 -66
- package/dist/src/utils/getCallablePromise.d.ts +0 -6
- package/dist/src/utils/getCallablePromise.js +0 -14
- package/dist/src/utils/log.d.ts +0 -8
- package/dist/src/utils/log.js +0 -26
- package/dist/src/utils/manifest-helpers.d.ts +0 -10
- package/dist/src/utils/manifest-helpers.js +0 -19
- package/dist/src/utils/non-interactive.d.ts +0 -14
- package/dist/src/utils/non-interactive.js +0 -15
- package/dist/src/utils/package-manager-detector.d.ts +0 -18
- package/dist/src/utils/package-manager-detector.js +0 -72
- package/dist/src/utils/parameter-resolver.d.ts +0 -179
- package/dist/src/utils/parameter-resolver.js +0 -1554
- package/dist/src/utils/retry.d.ts +0 -6
- package/dist/src/utils/retry.js +0 -21
- package/dist/src/utils/schema-formatter.d.ts +0 -11
- package/dist/src/utils/schema-formatter.js +0 -131
- package/dist/src/utils/serializeAsync.d.ts +0 -2
- package/dist/src/utils/serializeAsync.js +0 -16
- package/dist/src/utils/spinner.d.ts +0 -1
- package/dist/src/utils/spinner.js +0 -21
- package/dist/src/utils/string-utils.d.ts +0 -1
- package/dist/src/utils/string-utils.js +0 -6
- package/dist/src/utils/triggerDrain.d.ts +0 -144
- package/dist/src/utils/triggerDrain.js +0 -351
- package/dist/src/utils/version-checker.d.ts +0 -17
- package/dist/src/utils/version-checker.js +0 -154
- 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
|
-
}
|