zapier-platform-cli 15.16.1 → 15.17.0

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.
@@ -1,4 +1,6 @@
1
- const fs = require('node:fs');
1
+ const crypto = require('node:crypto');
2
+ const fs = require('node:fs/promises');
3
+ const http = require('node:http');
2
4
 
3
5
  const _ = require('lodash');
4
6
  const { flags } = require('@oclif/command');
@@ -11,7 +13,7 @@ const { DateTime, IANAZone } = require('luxon');
11
13
 
12
14
  const BaseCommand = require('../ZapierBaseCommand');
13
15
  const { buildFlags } = require('../buildFlags');
14
- const { localAppCommand } = require('../../utils/local');
16
+ const { localAppCommand, getLocalAppHandler } = require('../../utils/local');
15
17
  const { startSpinner, endSpinner } = require('../../utils/display');
16
18
 
17
19
  const ACTION_TYPE_PLURALS = {
@@ -224,6 +226,15 @@ const resolveInputDataTypes = (inputData, inputFields, timezone) => {
224
226
  return inputData;
225
227
  };
226
228
 
229
+ const appendEnv = async (vars, prefix = '') => {
230
+ await fs.appendFile(
231
+ '.env',
232
+ Object.entries(vars)
233
+ .filter(([k, v]) => v !== undefined)
234
+ .map(([k, v]) => `${prefix}${k}='${v || ''}'\n`)
235
+ );
236
+ };
237
+
227
238
  const testAuth = async (authData, meta, zcacheTestObj) => {
228
239
  startSpinner('Invoking authentication.test');
229
240
  const result = await localAppCommand({
@@ -231,7 +242,10 @@ const testAuth = async (authData, meta, zcacheTestObj) => {
231
242
  method: 'authentication.test',
232
243
  bundle: {
233
244
  authData,
234
- meta,
245
+ meta: {
246
+ ...meta,
247
+ isTestingAuth: true,
248
+ },
235
249
  },
236
250
  zcacheTestObj,
237
251
  customLogger,
@@ -300,7 +314,304 @@ const customLogger = (message, data) => {
300
314
  debug(data);
301
315
  };
302
316
 
317
+ const formatFieldDisplay = (field) => {
318
+ const ftype = field.type || 'string';
319
+ let result;
320
+ if (field.label) {
321
+ result = `${field.label} | ${field.key} | ${ftype}`;
322
+ } else {
323
+ result = `${field.key} | ${ftype}`;
324
+ }
325
+ if (field.required) {
326
+ result += ' | required';
327
+ }
328
+ return result;
329
+ };
330
+
303
331
  class InvokeCommand extends BaseCommand {
332
+ async promptForAuthFields(authFields) {
333
+ const authData = {};
334
+ for (const field of authFields) {
335
+ if (field.computed) {
336
+ continue;
337
+ }
338
+ const message = formatFieldDisplay(field) + ':';
339
+ let value;
340
+ if (field.type === 'password') {
341
+ value = await this.promptHidden(message, true);
342
+ } else {
343
+ value = await this.prompt(message, { useStderr: true });
344
+ }
345
+ authData[field.key] = value;
346
+ }
347
+ return authData;
348
+ }
349
+
350
+ async startBasicAuth(authFields) {
351
+ if (this.nonInteractive) {
352
+ throw new Error(
353
+ 'The `auth start` subcommand for "basic" authentication type only works in interactive mode.'
354
+ );
355
+ }
356
+ return this.promptForAuthFields([
357
+ {
358
+ key: 'username',
359
+ label: 'Username',
360
+ required: true,
361
+ },
362
+ {
363
+ key: 'password',
364
+ label: 'Password',
365
+ required: true,
366
+ },
367
+ ]);
368
+ }
369
+
370
+ async startCustomAuth(authFields) {
371
+ if (this.nonInteractive) {
372
+ throw new Error(
373
+ 'The `auth start` subcommand for "custom" authentication type only works in interactive mode.'
374
+ );
375
+ }
376
+ return this.promptForAuthFields(authFields);
377
+ }
378
+
379
+ async startOAuth2(appDefinition) {
380
+ const redirectUri = this.flags['redirect-uri'];
381
+ const port = this.flags['local-port'];
382
+ const env = {};
383
+
384
+ if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) {
385
+ if (this.nonInteractive) {
386
+ throw new Error(
387
+ 'CLIENT_ID and CLIENT_SECRET must be set in the .env file in non-interactive mode.'
388
+ );
389
+ } else {
390
+ console.warn(
391
+ 'CLIENT_ID and CLIENT_SECRET are required for OAuth2, ' +
392
+ "but they are not found in the .env file. I'll prompt you for them now."
393
+ );
394
+ }
395
+ }
396
+
397
+ if (!process.env.CLIENT_ID) {
398
+ env.CLIENT_ID = await this.prompt('CLIENT_ID:', { useStderr: true });
399
+ process.env.CLIENT_ID = env.CLIENT_ID;
400
+ }
401
+ if (!process.env.CLIENT_SECRET) {
402
+ env.CLIENT_SECRET = await this.prompt('CLIENT_SECRET:', {
403
+ useStderr: true,
404
+ });
405
+ process.env.CLIENT_SECRET = env.CLIENT_SECRET;
406
+ }
407
+
408
+ if (!_.isEmpty(env)) {
409
+ // process.env changed, so we need to reload the modules that have loaded
410
+ // the old values of process.env
411
+ getLocalAppHandler({ reload: true });
412
+
413
+ // Save envs so the user won't have to re-enter them if the command fails
414
+ await appendEnv(env);
415
+ console.warn('CLIENT_ID and CLIENT_SECRET saved to .env file.');
416
+ }
417
+
418
+ startSpinner('Invoking authentication.oauth2Config.authorizeUrl');
419
+
420
+ const stateParam = crypto.randomBytes(20).toString('hex');
421
+ let authorizeUrl = await localAppCommand({
422
+ command: 'execute',
423
+ method: 'authentication.oauth2Config.authorizeUrl',
424
+ bundle: {
425
+ inputData: {
426
+ response_type: 'code',
427
+ redirect_uri: redirectUri,
428
+ state: stateParam,
429
+ },
430
+ },
431
+ });
432
+ if (!authorizeUrl.includes('&scope=')) {
433
+ const scope = appDefinition.authentication.oauth2Config.scope;
434
+ if (scope) {
435
+ authorizeUrl += `&scope=${encodeURIComponent(scope)}`;
436
+ }
437
+ }
438
+ debug('authorizeUrl:', authorizeUrl);
439
+
440
+ endSpinner();
441
+ startSpinner('Starting local HTTP server');
442
+
443
+ let resolveCode;
444
+ const codePromise = new Promise((resolve) => {
445
+ resolveCode = resolve;
446
+ });
447
+
448
+ const server = http.createServer((req, res) => {
449
+ // Parse the request URL to extract the query parameters
450
+ const code = new URL(req.url, redirectUri).searchParams.get('code');
451
+ if (code) {
452
+ resolveCode(code);
453
+ debug(`Received code '${code}' from ${req.headers.referer}`);
454
+
455
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
456
+ res.end(
457
+ 'Parameter `code` received successfully. Go back to the terminal to continue.'
458
+ );
459
+ } else {
460
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
461
+ res.end(
462
+ 'Error: Did not receive `code` query parameter. ' +
463
+ 'Did you have the right CLIENT_ID and CLIENT_SECRET? ' +
464
+ 'Or did your server respond properly?'
465
+ );
466
+ }
467
+ });
468
+
469
+ await new Promise((resolve) => {
470
+ server.listen(port, resolve);
471
+ });
472
+
473
+ endSpinner();
474
+ startSpinner(
475
+ 'Opening browser to authorize (press Ctrl-C to exit on error)'
476
+ );
477
+
478
+ const { default: open } = await import('open');
479
+ open(authorizeUrl);
480
+
481
+ const code = await codePromise;
482
+ endSpinner();
483
+
484
+ startSpinner('Closing local HTTP server');
485
+ await new Promise((resolve) => {
486
+ server.close(resolve);
487
+ });
488
+ debug('Local HTTP server closed');
489
+
490
+ endSpinner();
491
+ startSpinner('Invoking authentication.oauth2Config.getAccessToken');
492
+
493
+ const authData = await localAppCommand({
494
+ command: 'execute',
495
+ method: 'authentication.oauth2Config.getAccessToken',
496
+ bundle: {
497
+ inputData: {
498
+ code,
499
+ redirect_uri: redirectUri,
500
+ },
501
+ },
502
+ });
503
+
504
+ endSpinner();
505
+ return authData;
506
+ }
507
+
508
+ async startSessionAuth(appDefinition) {
509
+ if (this.nonInteractive) {
510
+ throw new Error(
511
+ 'The `auth start` subcommand for "session" authentication type only works in interactive mode.'
512
+ );
513
+ }
514
+ const authData = await this.promptForAuthFields(
515
+ appDefinition.authentication.fields
516
+ );
517
+
518
+ startSpinner('Invoking authentication.sessionConfig.perform');
519
+ const sessionData = await localAppCommand({
520
+ command: 'execute',
521
+ method: 'authentication.sessionConfig.perform',
522
+ bundle: {
523
+ authData,
524
+ },
525
+ });
526
+ endSpinner();
527
+
528
+ return { ...authData, ...sessionData };
529
+ }
530
+
531
+ async startAuth(appDefinition) {
532
+ const authentication = appDefinition.authentication;
533
+ if (!authentication) {
534
+ console.warn(
535
+ "Your integration doesn't seem to need authentication. " +
536
+ "If that isn't true, the app definition should have " +
537
+ 'an `authentication` object at the root level.'
538
+ );
539
+ return null;
540
+ }
541
+ switch (authentication.type) {
542
+ case 'basic':
543
+ return this.startBasicAuth(authentication.fields);
544
+ case 'custom':
545
+ return this.startCustomAuth(authentication.fields);
546
+ case 'oauth2':
547
+ return this.startOAuth2(appDefinition);
548
+ case 'session':
549
+ return this.startSessionAuth(appDefinition);
550
+ default:
551
+ // TODO: Add support for 'digest' and 'oauth1'
552
+ throw new Error(
553
+ `This command doesn't support authentication type "${authentication.type}".`
554
+ );
555
+ }
556
+ }
557
+
558
+ async refreshOAuth2(appDefinition, authData) {
559
+ startSpinner('Invoking authentication.oauth2Config.refreshAccessToken');
560
+
561
+ const newAuthData = await localAppCommand({
562
+ command: 'execute',
563
+ method: 'authentication.oauth2Config.refreshAccessToken',
564
+ bundle: {
565
+ authData,
566
+ },
567
+ });
568
+
569
+ endSpinner();
570
+ return newAuthData;
571
+ }
572
+
573
+ async refreshSessionAuth(appDefinition, authData) {
574
+ startSpinner('Invoking authentication.sessionConfig.perform');
575
+
576
+ const sessionData = await localAppCommand({
577
+ command: 'execute',
578
+ method: 'authentication.sessionConfig.perform',
579
+ bundle: {
580
+ authData,
581
+ },
582
+ });
583
+
584
+ endSpinner();
585
+ return sessionData;
586
+ }
587
+
588
+ async refreshAuth(appDefinition, authData) {
589
+ const authentication = appDefinition.authentication;
590
+ if (!authentication) {
591
+ console.warn(
592
+ "Your integration doesn't seem to need authentication. " +
593
+ "If that isn't true, the app definition should have " +
594
+ 'an `authentication` object at the root level.'
595
+ );
596
+ return null;
597
+ }
598
+ if (_.isEmpty(authData)) {
599
+ throw new Error(
600
+ 'No auth data found in the .env file. Run `zapier invoke auth start` first to initialize the auth data.'
601
+ );
602
+ }
603
+ switch (authentication.type) {
604
+ case 'oauth2':
605
+ return this.refreshOAuth2(appDefinition, authData);
606
+ case 'session':
607
+ return this.refreshSessionAuth(appDefinition, authData);
608
+ default:
609
+ throw new Error(
610
+ `This command doesn't support refreshing authentication type "${authentication.type}".`
611
+ );
612
+ }
613
+ }
614
+
304
615
  async promptForField(
305
616
  field,
306
617
  appDefinition,
@@ -310,19 +621,7 @@ class InvokeCommand extends BaseCommand {
310
621
  zcacheTestObj,
311
622
  cursorTestObj
312
623
  ) {
313
- const ftype = field.type || 'string';
314
-
315
- let message;
316
- if (field.label) {
317
- message = `${field.label} | ${field.key} | ${ftype}`;
318
- } else {
319
- message = `${field.key} | ${ftype}`;
320
- }
321
- if (field.required) {
322
- message += ' | required';
323
- }
324
- message += ':';
325
-
624
+ const message = formatFieldDisplay(field) + ':';
326
625
  if (field.dynamic) {
327
626
  // Dyanmic dropdown
328
627
  const [triggerKey, idField, labelField] = field.dynamic.split('.');
@@ -358,7 +657,7 @@ class InvokeCommand extends BaseCommand {
358
657
  }),
359
658
  { useStderr: true }
360
659
  );
361
- } else if (ftype === 'boolean') {
660
+ } else if (field.type === 'boolean') {
362
661
  const yes = await this.confirm(message, false, !field.required, true);
363
662
  return yes ? 'yes' : 'no';
364
663
  } else {
@@ -378,10 +677,10 @@ class InvokeCommand extends BaseCommand {
378
677
  ) {
379
678
  const missingFields = getMissingRequiredInputFields(inputData, inputFields);
380
679
  if (missingFields.length) {
381
- if (!process.stdin.isTTY || meta.isFillingDynamicDropdown) {
680
+ if (this.nonInteractive || meta.isFillingDynamicDropdown) {
382
681
  throw new Error(
383
- 'You must specify these required fields in --inputData at least when stdin is not a TTY: \n' +
384
- missingFields.map((f) => `* ${f.key}`).join('\n')
682
+ "You're in non-interactive mode, so you must at least specify these required fields with --inputData: \n" +
683
+ missingFields.map((f) => '* ' + formatFieldDisplay(f)).join('\n')
385
684
  );
386
685
  }
387
686
  for (const f of missingFields) {
@@ -481,7 +780,7 @@ class InvokeCommand extends BaseCommand {
481
780
  zcacheTestObj,
482
781
  cursorTestObj
483
782
  );
484
- if (process.stdin.isTTY && !meta.isFillingDynamicDropdown) {
783
+ if (!this.nonInteractive && !meta.isFillingDynamicDropdown) {
485
784
  await this.promptForInputFieldEdit(
486
785
  inputData,
487
786
  inputFields,
@@ -581,14 +880,22 @@ class InvokeCommand extends BaseCommand {
581
880
  }
582
881
 
583
882
  async perform() {
584
- dotenv.config({ override: true });
883
+ const dotenvResult = dotenv.config({ override: true });
884
+ if (_.isEmpty(dotenvResult.parsed)) {
885
+ console.warn(
886
+ 'The .env file does not exist or is empty. ' +
887
+ 'You may need to set some environment variables in there if your code uses process.env.'
888
+ );
889
+ }
890
+
891
+ this.nonInteractive = this.flags['non-interactive'] || !process.stdin.isTTY;
585
892
 
586
893
  let { actionType, actionKey } = this.args;
587
894
 
588
895
  if (!actionType) {
589
- if (!process.stdin.isTTY) {
896
+ if (this.nonInteractive) {
590
897
  throw new Error(
591
- 'You must specify ACTIONTYPE and ACTIONKEY when stdin is not a TTY.'
898
+ 'You must specify ACTIONTYPE and ACTIONKEY in non-interactive mode.'
592
899
  );
593
900
  }
594
901
  actionType = await this.promptWithList(
@@ -602,11 +909,11 @@ class InvokeCommand extends BaseCommand {
602
909
  const appDefinition = await localAppCommand({ command: 'definition' });
603
910
 
604
911
  if (!actionKey) {
605
- if (!process.stdin.isTTY) {
606
- throw new Error('You must specify ACTIONKEY when stdin is not a TTY.');
912
+ if (this.nonInteractive) {
913
+ throw new Error('You must specify ACTIONKEY in non-interactive mode.');
607
914
  }
608
915
  if (actionType === 'auth') {
609
- const actionKeys = ['label', 'test'];
916
+ const actionKeys = ['label', 'refresh', 'start', 'test'];
610
917
  actionKey = await this.promptWithList(
611
918
  'Which auth operation would you like to invoke?',
612
919
  actionKeys,
@@ -644,7 +951,28 @@ class InvokeCommand extends BaseCommand {
644
951
  isTestingAuth: true,
645
952
  };
646
953
  switch (actionKey) {
647
- // TODO: Add 'start' and 'refresh' commands
954
+ case 'start': {
955
+ const newAuthData = await this.startAuth(appDefinition);
956
+ if (_.isEmpty(newAuthData)) {
957
+ return;
958
+ }
959
+ await appendEnv(newAuthData, AUTH_FIELD_ENV_PREFIX);
960
+ console.warn(
961
+ 'Auth data appended to .env file. Run `zapier invoke auth test` to test it.'
962
+ );
963
+ return;
964
+ }
965
+ case 'refresh': {
966
+ const newAuthData = await this.refreshAuth(appDefinition, authData);
967
+ if (_.isEmpty(newAuthData)) {
968
+ return;
969
+ }
970
+ await appendEnv(newAuthData, AUTH_FIELD_ENV_PREFIX);
971
+ console.warn(
972
+ 'Auth data has been refreshed and appended to .env file. Run `zapier invoke auth test` to test it.'
973
+ );
974
+ return;
975
+ }
648
976
  case 'test': {
649
977
  const output = await testAuth(authData, meta, zcacheTestObj);
650
978
  console.log(JSON.stringify(output, null, 2));
@@ -654,7 +982,7 @@ class InvokeCommand extends BaseCommand {
654
982
  const labelTemplate = appDefinition.authentication.connectionLabel;
655
983
  if (labelTemplate && labelTemplate.startsWith('$func$')) {
656
984
  console.warn(
657
- 'Function-based connection label is currently not supported; will print auth test result instead.'
985
+ 'Function-based connection label is not supported yet. Printing auth test result instead.'
658
986
  );
659
987
  const output = await testAuth(authData, meta, zcacheTestObj);
660
988
  console.log(JSON.stringify(output, null, 2));
@@ -674,7 +1002,10 @@ class InvokeCommand extends BaseCommand {
674
1002
  return;
675
1003
  }
676
1004
  default:
677
- throw new Error(`Unknown auth action: ${actionKey}`);
1005
+ throw new Error(
1006
+ `Unknown auth operation "${actionKey}". ` +
1007
+ 'The options are "label", "refresh", "start", and "test". \n'
1008
+ );
678
1009
  }
679
1010
  } else {
680
1011
  const action = appDefinition[actionTypePlural][actionKey];
@@ -694,7 +1025,8 @@ class InvokeCommand extends BaseCommand {
694
1025
  if (filePath === '-') {
695
1026
  inputStream = process.stdin;
696
1027
  } else {
697
- inputStream = fs.createReadStream(filePath, { encoding: 'utf8' });
1028
+ const fd = await fs.open(filePath);
1029
+ inputStream = fd.createReadStream({ encoding: 'utf8' });
698
1030
  }
699
1031
  inputData = await readStream(inputStream);
700
1032
  }
@@ -746,14 +1078,14 @@ InvokeCommand.flags = buildFlags({
746
1078
  description:
747
1079
  'The input data to pass to the action. Must be a JSON-encoded object. The data can be passed from the command directly like \'{"key": "value"}\', read from a file like @file.json, or read from stdin like @-.',
748
1080
  }),
749
- isLoadingSample: flags.boolean({
1081
+ isFillingDynamicDropdown: flags.boolean({
750
1082
  description:
751
- 'Set bundle.meta.isLoadingSample to true. When true in production, this run is initiated by the user in the Zap editor trying to pull a sample.',
1083
+ 'Set bundle.meta.isFillingDynamicDropdown to true. Only makes sense for a polling trigger. When true in production, this poll is being used to populate a dynamic dropdown.',
752
1084
  default: false,
753
1085
  }),
754
- isFillingDynamicDropdown: flags.boolean({
1086
+ isLoadingSample: flags.boolean({
755
1087
  description:
756
- 'Set bundle.meta.isFillingDynamicDropdown to true. Only makes sense for a polling trigger. When true in production, this poll is being used to populate a dynamic dropdown.',
1088
+ 'Set bundle.meta.isLoadingSample to true. When true in production, this run is initiated by the user in the Zap editor trying to pull a sample.',
757
1089
  default: false,
758
1090
  }),
759
1091
  isPopulatingDedupe: flags.boolean({
@@ -772,12 +1104,26 @@ InvokeCommand.flags = buildFlags({
772
1104
  'Set bundle.meta.page. Only makes sense for a trigger. When used in production, this indicates which page of items you should fetch. First page is 0.',
773
1105
  default: 0,
774
1106
  }),
1107
+ 'non-interactive': flags.boolean({
1108
+ description: 'Do not show interactive prompts.',
1109
+ default: false,
1110
+ }),
775
1111
  timezone: flags.string({
776
1112
  char: 'z',
777
1113
  description:
778
- 'Set the default timezone for datetime fields. If not set, defaults to America/Chicago, which matches Zapier production behavior. Find the list timezone names at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.',
1114
+ 'Set the default timezone for datetime field interpretation. If not set, defaults to America/Chicago, which matches Zapier production behavior. Find the list timezone names at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.',
779
1115
  default: 'America/Chicago',
780
1116
  }),
1117
+ 'redirect-uri': flags.string({
1118
+ description:
1119
+ "Only used by `auth start` subcommand. The redirect URI that will be passed to the OAuth2 authorization URL. Usually this should match the one configured in your server's OAuth2 application settings. A local HTTP server will be started to listen for the OAuth2 callback. If your server requires a non-localhost or HTTPS address for the redirect URI, you can set up port forwarding to route the non-localhost or HTTPS address to localhost.",
1120
+ default: 'http://localhost:9000',
1121
+ }),
1122
+ 'local-port': flags.integer({
1123
+ description:
1124
+ 'Only used by `auth start` subcommand. The local port that will be used to start the local HTTP server to listen for the OAuth2 callback. This port can be different from the one in the redirect URI if you have port forwarding set up.',
1125
+ default: 9000,
1126
+ }),
781
1127
  },
782
1128
  });
783
1129
 
@@ -790,14 +1136,16 @@ InvokeCommand.args = [
790
1136
  {
791
1137
  name: 'actionKey',
792
1138
  description:
793
- 'The trigger/action key you want to invoke. If ACTIONTYPE is "auth", this can be "test" or "label".',
1139
+ 'The trigger/action key you want to invoke. If ACTIONTYPE is "auth", this can be "label", "refresh", "start", or "test".',
794
1140
  },
795
1141
  ];
796
1142
 
797
- InvokeCommand.skipValidInstallCheck = true;
798
1143
  InvokeCommand.examples = [
799
1144
  'zapier invoke',
1145
+ 'zapier invoke auth start',
1146
+ 'zapier invoke auth refresh',
800
1147
  'zapier invoke auth test',
1148
+ 'zapier invoke auth label',
801
1149
  'zapier invoke trigger new_recipe',
802
1150
  `zapier invoke create add_recipe --inputData '{"title": "Pancakes"}'`,
803
1151
  'zapier invoke search find_recipe -i @file.json',
@@ -807,11 +1155,23 @@ InvokeCommand.description = `Invoke an auth operation, a trigger, or a create/se
807
1155
 
808
1156
  This command emulates how Zapier production environment would invoke your integration. It runs code locally, so you can use this command to quickly test your integration without deploying it to Zapier. This is especially useful for debugging and development.
809
1157
 
810
- This command loads \`authData\` from the \`.env\` file in the current directory. Create a \`.env\` file with the necessary auth data before running this command. Each line in \`.env\` should be in the format \`authData_FIELD_KEY=VALUE\`. For example, an OAuth2 integration might have a \`.env\` file like this:
1158
+ This command loads environment variables and \`authData\` from the \`.env\` file in the current directory. If you don't have a \`.env\` file yet, you can use the \`zapier invoke auth start\` command to help you initialize it, or you can manually create it.
1159
+
1160
+ The \`zapier invoke auth start\` subcommand will prompt you for the necessary auth fields and save them to the \`.env\` file. For OAuth2, it will start a local HTTP server, open the authorization URL in the browser, wait for the OAuth2 redirect, and get the access token.
1161
+
1162
+ Each line in the \`.env\` file should follow one of these formats:
1163
+
1164
+ * \`VAR_NAME=VALUE\` for environment variables
1165
+ * \`authData_FIELD_KEY=VALUE\` for auth data fields
1166
+
1167
+ For example, a \`.env\` file for an OAuth2 integration might look like this:
811
1168
 
812
1169
  \`\`\`
813
- authData_access_token=1234567890
814
- authData_other_auth_field=abcdef
1170
+ CLIENT_ID='your_client_id'
1171
+ CLIENT_SECRET='your_client_secret'
1172
+ authData_access_token='1234567890'
1173
+ authData_refresh_token='abcdefg'
1174
+ authData_account_name='zapier'
815
1175
  \`\`\`
816
1176
 
817
1177
  To test if the auth data is correct, run either one of these:
@@ -821,7 +1181,9 @@ zapier invoke auth test # invokes authentication.test method
821
1181
  zapier invoke auth label # invokes authentication.test and renders connection label
822
1182
  \`\`\`
823
1183
 
824
- Then you can test an trigger, a search, or a create action. For example, this is how you invoke a trigger with key \`new_recipe\`:
1184
+ To refresh stale auth data for OAuth2 or session auth, run \`zapier invoke auth refresh\`.
1185
+
1186
+ Once you have the correct auth data, you can test an trigger, a search, or a create action. For example, here's how you invoke a trigger with the key \`new_recipe\`:
825
1187
 
826
1188
  \`\`\`
827
1189
  zapier invoke trigger new_recipe
@@ -829,10 +1191,12 @@ zapier invoke trigger new_recipe
829
1191
 
830
1192
  To add input data, use the \`--inputData\` flag. The input data can come from the command directly, a file, or stdin. See **EXAMPLES** below.
831
1193
 
832
- The following are current limitations and may be supported in the future:
1194
+ When you miss any command arguments, such as ACTIONTYPE or ACTIONKEY, the command will prompt you interactively. If you don't want to get interactive prompts, use the \`--non-interactive\` flag.
1195
+
1196
+ The \`--debug\` flag will show you the HTTP request logs and any console logs you have in your code.
1197
+
1198
+ The following is a non-exhaustive list of current limitations and may be supported in the future:
833
1199
 
834
- - \`zapier invoke auth start\` to help you initialize the auth data in \`.env\`
835
- - \`zapier invoke auth refresh\` to refresh the auth data in \`.env\`
836
1200
  - Hook triggers, including REST hook subscribe/unsubscribe
837
1201
  - Line items
838
1202
  - Output hydration
@@ -840,6 +1204,10 @@ The following are current limitations and may be supported in the future:
840
1204
  - Dynamic dropdown pagination
841
1205
  - Function-based connection label
842
1206
  - Buffered create actions
1207
+ - Search-or-create actions
1208
+ - Search-powered fields
1209
+ - Field choices
1210
+ - autoRefresh for OAuth2 and session auth
843
1211
  `;
844
1212
 
845
1213
  module.exports = InvokeCommand;
@@ -251,6 +251,7 @@ const renderIndex = async (appDefinition) => {
251
251
  triggers: 'Trigger',
252
252
  creates: 'Create',
253
253
  searches: 'Search',
254
+ bulkReads: 'BulkRead',
254
255
  },
255
256
  (importNameSuffix, stepType) => {
256
257
  _.each(appDefinition[stepType], (definition, key) => {
@@ -380,7 +381,7 @@ const convertApp = async (appInfo, appDefinition, newAppDir) => {
380
381
 
381
382
  const promises = [];
382
383
 
383
- ['triggers', 'creates', 'searches'].forEach((stepType) => {
384
+ ['triggers', 'creates', 'searches', 'bulkReads'].forEach((stepType) => {
384
385
  _.each(appDefinition[stepType], (definition, key) => {
385
386
  promises.push(
386
387
  writeStep(stepType, definition, key, newAppDir),