validate
diff --git a/packages/cli/docs/cli.md b/packages/cli/docs/cli.md
index 455a3f1df..9822e981e 100644
--- a/packages/cli/docs/cli.md
+++ b/packages/cli/docs/cli.md
@@ -53,50 +53,6 @@ This command is typically followed by `zapier upload`.
* `-d, --debug` | Show extra debugging output
-## collaborate
-
- > Manage the admins on your project. Can optionally --remove.
-
- **Usage:** `zapier collaborate [user@example.com]`
-
-
-Give any user registered on Zapier the ability to collaborate on your app. Commonly, this is useful for teammates, contractors, or other developers who might want to make changes on your app. Only admin access is supported. If you'd only like to provide read-only or testing access, try `zapier invite`.
-
-**Arguments**
-
-* _none_ -- print all admins
-* `email [user@example.com]` -- _optional_, which user to add/remove
-* `--remove` -- _optional_, elect to remove this user
-* `--format={plain,json,raw,row,table}` -- _optional_, display format. Default is `table`
-* `--help` -- _optional_, prints this help text
-* `--debug` -- _optional_, print debug API calls and tracebacks
-
-```bash
-$ zapier collaborate
-# The admins on your app "Example" listed below.
-#
-# ┌──────────────────┬───────┬──────────┐
-# │ Email │ Role │ Status │
-# ├──────────────────┼───────┼──────────┤
-# │ user@example.com │ admin │ accepted │
-# └──────────────────┴───────┴──────────┘
-
-$ zapier collaborate user@example.com
-# Preparing to add admin user@example.com to your app "Example".
-#
-# Adding user@example.com - done!
-#
-# Admins updated! Try viewing them with `zapier collaborate`.
-
-$ zapier collaborate user@example.com --remove
-# Preparing to remove admin user@example.com from your app "Example".
-#
-# Removing user@example.com - done!
-#
-# Admins updated! Try viewing them with `zapier collaborate`.
-```
-
-
## convert
> Converts a Legacy Web Builder or Visual Builder app to a CLI app.
@@ -400,57 +356,6 @@ This command also checks the current directory for a linked integration.
* `apps`
-## invite
-
- > Manage the invitees/testers on your project. Can optionally specify a version or --remove.
-
- **Usage:** `zapier invite [user@example.com] [1.0.0]`
-
-
-Invite any user registered on Zapier to test your app. Commonly, this is useful for teammates, contractors, or other team members who might want to test, QA, or view your app versions. If you'd like to provide full admin access, try `zapier collaborate`.
-
-> Send an email directly, which contains a one-time use link for them only - or share the public URL to "bulk" invite folks!
-
-**Arguments**
-
-* _none_ -- print all invitees
-* `email [user@example.com]` -- _optional_, which user to add/remove
-* `version [1.0.0]` -- _optional_, only invite to a specific version
-* `--remove` -- _optional_, elect to remove this user
-* `--format={plain,json,raw,row,table}` -- _optional_, display format. Default is `table`
-* `--help` -- _optional_, prints this help text
-* `--debug` -- _optional_, print debug API calls and tracebacks
-
-```bash
-$ zapier invite
-# The invitees on your app listed below.
-
-# ┌───────────────────┬──────────┬──────────┬─────────┐
-# │ Email │ Role │ Status │ Version │
-# ├───────────────────┼──────────┼──────────┼─────────┤
-# │ user@example.com │ invitees │ accepted │ 1.0.0 │
-# └───────────────────┴──────────┴──────────┴─────────┘
-#
-# Don't want to send individual invite emails? Use this public link and share with anyone on the web:
-#
-# https://zapier.com/platform/public-invite/1/222dcd03aed943a8676dc80e2427a40d/
-
-$ zapier invite user@example.com 1.0.0
-# Preparing to add invitee user@example.com to your app "Example (1.0.0)".
-#
-# Adding user@example.com - done!
-#
-# Invitees updated! Try viewing them with `zapier invite`.
-
-$ zapier invite user@example.com --remove
-# Preparing to remove invitee user@example.com from your app "Example".
-#
-# Removing user@example.com - done!
-#
-# Invitees updated! Try viewing them with `zapier invite`.
-```
-
-
## link
> Link the current directory to an app you have access to.
@@ -705,6 +610,73 @@ You can mix and match several options to customize the created scaffold for your
* `zapier scaffold trigger "Existing Create" --force`
+## team:add
+
+> Add a team member to your integration.
+
+**Usage**: `zapier team:add EMAIL ROLE [MESSAGE]`
+
+These users come in two levels:
+
+ * Admins, who can edit everything about the app
+
+ * Subscribers, who can't directly access the app, but will receive periodic email updates. These updates include quarterly health socores and more.
+
+Team members can be freely added and removed.
+
+**Arguments**
+* (required) `email` | The user to be invited. If they don't have a Zapier account, they'll be prompted to create one.
+* (required) `role` | The level the invited team member should be at. Admins can edit everything and get email updates. Subscribers only get email updates.
+* `message` | A message sent in the email to your team member, if you need to provide context. Wrap the message in quotes to ensure spaces get saved.
+
+**Flags**
+* `-d, --debug` | Show extra debugging output
+
+**Examples**
+* `zapier team:add bruce@wayne.com admin`
+* `zapier team:add alfred@wayne.com subscriber "Hey Alfred, check out this app."`
+
+**Aliases**
+* `team:invite`
+
+
+## team:get
+
+> Get a list of team members involved with your app.
+
+**Usage**: `zapier team:get`
+
+These users come in two levels:
+
+ * Admins, who can edit everything about the app
+
+ * Subscribers, who can't directly access the app, but will receive periodic email updates. These updates include quarterly health socores and more.
+
+Use the `zapier team:add` and `zapier team:remove` commands to modify your team.
+
+**Flags**
+* `-f, --format` | undefined One of `[plain | json | raw | row | table]`. Defaults to `table`.
+* `-d, --debug` | Show extra debugging output
+
+**Aliases**
+* `team:list`
+
+
+## team:remove
+
+> Remove a team member from all versions of your integration.
+
+**Usage**: `zapier team:remove`
+
+Admins will immediately lose write access to the app. Subscribers won't receive future email updates.
+
+**Flags**
+* `-d, --debug` | Show extra debugging output
+
+**Aliases**
+* `team:delete`
+
+
## test
> Tests your integration via `npm test`.
@@ -739,6 +711,74 @@ This command sends both build/build.zip and build/source.zip to Zapier for use.
* `-d, --debug` | Show extra debugging output
+## users:add
+
+> Add a user to some or all versions of your integration.
+
+**Usage**: `zapier users:add EMAIL [VERSION]`
+
+When this command is run, we'll send an email to the user inviting them to try your app. You can track the status of that invite using the `zapier users:get` command.
+
+Invited users will be able to see your integration's name, logo, and description. They'll also be able to create Zaps using any available triggers and actions.
+
+**Arguments**
+* (required) `email` | The user to be invited. If they don't have a Zapier account, they'll be prompted to create one.
+* `version` | A version string (like 1.2.3). Optional, used only if you want to invite a user to a specific version instead of all versions.
+
+**Flags**
+* `-f, --force` | Skip confirmation. Useful for running programatically.
+* `-d, --debug` | Show extra debugging output
+
+**Aliases**
+* `users:invite`
+
+
+## users:get
+
+> Get a list of users who have been invited to your app.
+
+**Usage**: `zapier users:get`
+
+Note that this list of users is NOT a comprehensive list of everyone who is using your integration. It only includes users who were invited directly by email (using the `zapier users:add` command or the web UI). Users who joined by clicking links generated using the `zapier user:links` command won't show up here.
+
+**Flags**
+* `-f, --format` | undefined One of `[plain | json | raw | row | table]`. Defaults to `table`.
+* `-d, --debug` | Show extra debugging output
+
+**Aliases**
+* `users:list`
+
+
+## users:links
+
+> Get a list of links that are used to invite users to your app.
+
+**Usage**: `zapier users:links`
+
+**Flags**
+* `-f, --format` | undefined One of `[plain | json | raw | row | table]`. Defaults to `table`.
+* `-d, --debug` | Show extra debugging output
+
+
+## users:remove
+
+> Remove a user from all versions of your integration.
+
+**Usage**: `zapier users:remove EMAIL`
+
+When this command is run, their Zaps will immediately turn off. They won't be able to use your app again until they're re-invited or it has gone public. In practice, this command isn't run often as it's very disruptive to users.
+
+**Arguments**
+* (required) `email` | The user to be removed.
+
+**Flags**
+* `-f, --force` | Skip confirmation. Useful for running programatically.
+* `-d, --debug` | Show extra debugging output
+
+**Aliases**
+* `users:delete`
+
+
## validate
> Validates your Zapier app.
diff --git a/packages/cli/src/commands/_access.js b/packages/cli/src/commands/_access.js
deleted file mode 100644
index 69daab2cd..000000000
--- a/packages/cli/src/commands/_access.js
+++ /dev/null
@@ -1,120 +0,0 @@
-const _ = require('lodash');
-const colors = require('colors');
-
-const utils = require('../utils');
-
-const makeAccess = (command, recordType) => {
- const recordTypePlural = `${recordType}s`;
-
- const access = (context, email, version) => {
- if (email) {
- return utils
- .checkCredentials()
- .then(() => utils.getLinkedApp())
- .then(app => {
- const urlExtra =
- version && !global.argOpts.remove ? `/${version}` : '';
- const msgExtra = version ? ` (${version})` : '';
- const url = `/apps/${app.id}/${recordTypePlural}/${email}${urlExtra}`;
-
- if (global.argOpts.remove) {
- context.line(
- `Preparing to remove ${recordType} ${email} from your app "${
- app.title
- }".\n`
- );
- utils.startSpinner(`Removing ${email}`);
- return utils.callAPI(url, { method: 'DELETE' });
- } else {
- context.line(
- `Preparing to add ${recordType} ${email} to your app "${
- app.title
- }${msgExtra}".\n`
- );
- utils.startSpinner(`Adding ${email}`);
- return utils.callAPI(url, { method: 'POST' });
- }
- })
- .then(() => {
- utils.endSpinner();
- context.line(
- `\n${_.capitalize(
- recordTypePlural
- )} updated! Try viewing them with \`zapier ${command}\`.`
- );
- });
- } else {
- return utils.listEndpoint(recordTypePlural).then(data => {
- context.line(
- `The ${recordTypePlural} on your app "${
- data.app.title
- }" listed below.\n`
- );
- const ifEmpty = colors.grey(
- `${_.capitalize(
- recordTypePlural
- )} not found. Try adding one with \`zapier ${command} user@example.com\`.`
- );
- const columns = [
- ['Email', 'email'],
- ['Role', 'role'],
- ['Status', 'status']
- ];
-
- // Invitees can get access to specific versions only
- if (recordType === 'invitee') {
- columns.push(['Version', 'app_version']);
- // Clean up "null" in app_version
- _.each(
- _.get(data, 'invitees', []),
- invitee => (invitee.app_version = invitee.app_version || 'All')
- );
- }
- utils.printData(data[recordTypePlural], columns, ifEmpty);
-
- if (data && data.invite_url) {
- context.line();
- context.line(
- 'You can invite users to all versions of this app (current and future) using this URL:\n\n ' +
- colors.bold(data.invite_url)
- );
- }
-
- if (
- data &&
- data.versions_invite_urls &&
- Object.keys(data.versions_invite_urls).length
- ) {
- context.line();
- context.line('Or you can invite users to a specific version:\n');
- _.each(data.versions_invite_urls, (inviteUrl, _version) => {
- context.line(` ${_version}: ${colors.bold(inviteUrl)}`);
- });
- }
- });
- }
- };
- access.argsSpec = [
- {
- name: 'email',
- help: 'which user to add/remove',
- example: 'user@example.com'
- }
- ];
- access.argOptsSpec = {
- remove: { flag: true, help: 'elect to remove this user' }
- };
-
- // Invitees can get access to specific versions only
- if (recordType === 'invitee') {
- access.argsSpec.push({
- name: 'version',
- help: 'only invite to a specific version',
- example: '1.0.0'
- });
- }
-
- return access;
-};
-
-module.exports = makeAccess;
diff --git a/packages/cli/src/commands/collaborate.js b/packages/cli/src/commands/collaborate.js
deleted file mode 100644
index f1b73f097..000000000
--- a/packages/cli/src/commands/collaborate.js
+++ /dev/null
@@ -1,45 +0,0 @@
-const utils = require('../utils');
-
-const makeAccess = require('./_access');
-
-const collaborate = makeAccess('collaborate', 'collaborator');
-collaborate.help =
- 'Manage the admins on your project. Can optionally --remove.';
-collaborate.example = 'zapier collaborate [user@example.com]';
-collaborate.docs = `
-Give any user registered on Zapier the ability to collaborate on your app. Commonly, this is useful for teammates, contractors, or other developers who might want to make changes on your app. Only admin access is supported. If you'd only like to provide read-only or testing access, try \`zapier invite\`.
-
-**Arguments**
-
-* _none_ -- print all admins
-${utils.argsFragment(collaborate.argsSpec)}
-${utils.argOptsFragment(collaborate.argOptsSpec)}
-${utils.defaultArgOptsFragment()}
-
-${'```'}bash
-$ zapier collaborate
-# The admins on your app "Example" listed below.
-#
-# ┌──────────────────┬───────┬──────────┐
-# │ Email │ Role │ Status │
-# ├──────────────────┼───────┼──────────┤
-# │ user@example.com │ admin │ accepted │
-# └──────────────────┴───────┴──────────┘
-
-$ zapier collaborate user@example.com
-# Preparing to add admin user@example.com to your app "Example".
-#
-# Adding user@example.com - done!
-#
-# Admins updated! Try viewing them with \`zapier collaborate\`.
-
-$ zapier collaborate user@example.com --remove
-# Preparing to remove admin user@example.com from your app "Example".
-#
-# Removing user@example.com - done!
-#
-# Admins updated! Try viewing them with \`zapier collaborate\`.
-${'```'}
-`;
-
-module.exports = collaborate;
diff --git a/packages/cli/src/commands/index.js b/packages/cli/src/commands/index.js
index 1bd75ef7a..90a07971c 100644
--- a/packages/cli/src/commands/index.js
+++ b/packages/cli/src/commands/index.js
@@ -1,10 +1,8 @@
module.exports = {
- collaborate: require('./collaborate'),
convert: require('./convert'),
delete: require('./delete'),
describe: require('./describe'),
help: require('./help'),
- invite: require('./invite'),
link: require('./link'),
logs: require('./logs'),
register: require('./register')
diff --git a/packages/cli/src/commands/invite.js b/packages/cli/src/commands/invite.js
deleted file mode 100644
index 2409c5d06..000000000
--- a/packages/cli/src/commands/invite.js
+++ /dev/null
@@ -1,51 +0,0 @@
-const utils = require('../utils');
-
-const makeAccess = require('./_access');
-
-const invite = makeAccess('invite', 'invitee');
-invite.help =
- 'Manage the invitees/testers on your project. Can optionally specify a version or --remove.';
-invite.example = 'zapier invite [user@example.com] [1.0.0]';
-invite.docs = `
-Invite any user registered on Zapier to test your app. Commonly, this is useful for teammates, contractors, or other team members who might want to test, QA, or view your app versions. If you'd like to provide full admin access, try \`zapier collaborate\`.
-
-> Send an email directly, which contains a one-time use link for them only - or share the public URL to "bulk" invite folks!
-
-**Arguments**
-
-* _none_ -- print all invitees
-${utils.argsFragment(invite.argsSpec)}
-${utils.argOptsFragment(invite.argOptsSpec)}
-${utils.defaultArgOptsFragment()}
-
-${'```'}bash
-$ zapier invite
-# The invitees on your app listed below.
-
-# ┌───────────────────┬──────────┬──────────┬─────────┐
-# │ Email │ Role │ Status │ Version │
-# ├───────────────────┼──────────┼──────────┼─────────┤
-# │ user@example.com │ invitees │ accepted │ 1.0.0 │
-# └───────────────────┴──────────┴──────────┴─────────┘
-#
-# Don't want to send individual invite emails? Use this public link and share with anyone on the web:
-#
-# https://zapier.com/platform/public-invite/1/222dcd03aed943a8676dc80e2427a40d/
-
-$ zapier invite user@example.com 1.0.0
-# Preparing to add invitee user@example.com to your app "Example (1.0.0)".
-#
-# Adding user@example.com - done!
-#
-# Invitees updated! Try viewing them with \`zapier invite\`.
-
-$ zapier invite user@example.com --remove
-# Preparing to remove invitee user@example.com from your app "Example".
-#
-# Removing user@example.com - done!
-#
-# Invitees updated! Try viewing them with \`zapier invite\`.
-${'```'}
-`;
-
-module.exports = invite;
diff --git a/packages/cli/src/oclif/ZapierBaseCommand.js b/packages/cli/src/oclif/ZapierBaseCommand.js
index 511c8021b..203402545 100644
--- a/packages/cli/src/oclif/ZapierBaseCommand.js
+++ b/packages/cli/src/oclif/ZapierBaseCommand.js
@@ -90,7 +90,6 @@ class ZapierBaseCommand extends Command {
}
}
- // we may not end up needing this
logJSON(o) {
if (typeof o === 'string') {
console.log(o);
@@ -100,7 +99,7 @@ class ZapierBaseCommand extends Command {
}
/**
- * log data in table form
+ * log data in table form. Headers are `[header, key]`
* @param {Object} opts
* @param {any[]} opts.rows The data to display
* @param {string[][]} opts.headers Array of pairs of the column header and the key in the row that that header applies to
@@ -111,10 +110,10 @@ class ZapierBaseCommand extends Command {
rows = [],
headers = [],
emptyMessage = '',
- usedRowBasedTable = false
+ formatOverride = ''
} = {}) {
- const formatter = usedRowBasedTable
- ? formatStyles.row
+ const formatter = formatOverride
+ ? formatStyles[formatOverride]
: formatStyles[this.flags.format];
if (!formatter) {
// throwing this error ensures that all commands that call this function take a format flag, since that provides the default
@@ -157,6 +156,11 @@ class ZapierBaseCommand extends Command {
return this.prompt(message, { default: defaultAns, type: 'confirm' });
}
+ // see here for options for choices: https://github.com/SBoudrias/Inquirer.js/#question
+ promptWithList(question, choices) {
+ return this.prompt(question, { type: 'list', choices });
+ }
+
/**
* should only print to stdout when in a non-data mode
*/
diff --git a/packages/cli/src/oclif/commands/team/add.js b/packages/cli/src/oclif/commands/team/add.js
new file mode 100644
index 000000000..d8bcb603c
--- /dev/null
+++ b/packages/cli/src/oclif/commands/team/add.js
@@ -0,0 +1,84 @@
+const ZapierBaseCommand = require('../../ZapierBaseCommand');
+// const { flags } = require('@oclif/command');
+const { cyan } = require('colors/safe');
+const { buildFlags } = require('../../buildFlags');
+const { callAPI, getLinkedApp } = require('../../../utils/api');
+
+const inviteMessage = (roleIsAdmin, title) =>
+ roleIsAdmin
+ ? `I would like you to help manage ${title}'s Zapier integration and get access to see how it's performing.`
+ : `I would like you to get reports and updates about ${title}'s Zapier integration.`;
+
+class TeamAddCommand extends ZapierBaseCommand {
+ async perform() {
+ this.startSpinner('Checking app');
+ const { id, title } = await getLinkedApp();
+ this.stopSpinner();
+
+ const roleIsAdmin = this.args.role === 'admin';
+ const message = this.args.message || inviteMessage(roleIsAdmin, title);
+
+ if (
+ !this.flags.force &&
+ !(await this.confirm(
+ `About to invite ${cyan(this.args.email)} to as a team member at the ${
+ this.args.role
+ } level. An email will be sent with the following message:\n\n"${message}"\n\nIs that ok?`,
+ true
+ ))
+ ) {
+ this.log('\ncancelled');
+ return;
+ }
+
+ this.startSpinner('Inviting Team Member');
+
+ const url = roleIsAdmin
+ ? `/apps/${id}/collaborators`
+ : `https://zapier.com/api/platform/v3/integrations/${id}/subscribers`;
+ await callAPI(url, {
+ url: url.startsWith('http') ? url : undefined,
+ method: 'POST',
+ body: { email: this.args.email, message }
+ });
+ this.stopSpinner();
+ }
+}
+
+TeamAddCommand.args = [
+ {
+ name: 'email',
+ description:
+ "The user to be invited. If they don't have a Zapier account, they'll be prompted to create one.",
+ required: true
+ },
+ {
+ name: 'role',
+ description:
+ 'The level the invited team member should be at. Admins can edit everything and get email updates. Subscribers only get email updates.',
+ options: ['admin', 'subscriber'],
+ required: true
+ },
+ {
+ name: 'message',
+ description:
+ 'A message sent in the email to your team member, if you need to provide context. Wrap the message in quotes to ensure spaces get saved.'
+ }
+];
+TeamAddCommand.flags = buildFlags();
+TeamAddCommand.description = `Add a team member to your integration.
+
+These users come in two levels:
+
+ * Admins, who can edit everything about the app
+ * Subscribers, who can't directly access the app, but will receive periodic email updates. These updates include quarterly health socores and more.
+
+Team members can be freely added and removed.`;
+
+TeamAddCommand.examples = [
+ 'zapier team:add bruce@wayne.com admin',
+ 'zapier team:add alfred@wayne.com subscriber "Hey Alfred, check out this app."'
+];
+TeamAddCommand.aliases = ['team:invite'];
+
+module.exports = TeamAddCommand;
diff --git a/packages/cli/src/oclif/commands/team/get.js b/packages/cli/src/oclif/commands/team/get.js
new file mode 100644
index 000000000..9e02a4ac6
--- /dev/null
+++ b/packages/cli/src/oclif/commands/team/get.js
@@ -0,0 +1,62 @@
+const ZapierBaseCommand = require('../../ZapierBaseCommand');
+const { cyan } = require('colors/safe');
+const { listEndpointMulti } = require('../../../utils/api');
+const { buildFlags } = require('../../buildFlags');
+const { BASE_ENDPOINT } = require('../../../constants');
+
+class TeamListCommand extends ZapierBaseCommand {
+ async perform() {
+ this.startSpinner('Loading team members');
+ const { admins, subscribers } = await listEndpointMulti(
+ { endpoint: 'collaborators', keyOverride: 'admins' },
+ {
+ endpoint: app =>
+ `${BASE_ENDPOINT}/api/platform/v3/integrations/${app.id}/subscribers`,
+ keyOverride: 'subscribers'
+ }
+ );
+
+ this.stopSpinner();
+
+ const cleanedUsers = [...admins, ...subscribers].map(
+ ({ status, name, role, email }) => ({
+ status,
+ name,
+ role: role === 'collaborator' ? 'admin' : 'subscriber',
+ email
+ })
+ );
+
+ this.logTable({
+ rows: cleanedUsers,
+ headers: [
+ ['Name', 'name'],
+ ['Role', 'role'],
+ ['Status', 'status'],
+ ['Email', 'email']
+ ]
+ });
+
+ this.log(
+ `To invite more team members, use the \`${cyan(
+ 'zapier team:add'
+ )}\` command.`
+ );
+ }
+}
+
+TeamListCommand.flags = buildFlags({ opts: { format: true } });
+TeamListCommand.description = `Get a list of team members involved with your app.
+
+These users come in two levels:
+
+ * Admins, who can edit everything about the app
+ * Subscribers, who can't directly access the app, but will receive periodic email updates. These updates include quarterly health socores and more.
+
+Use the \`${cyan('zapier team:add')}\` and \`${cyan(
+ 'zapier team:remove'
+)}\` commands to modify your team.
+`;
+TeamListCommand.aliases = ['team:list'];
+
+module.exports = TeamListCommand;
diff --git a/packages/cli/src/oclif/commands/team/remove.js b/packages/cli/src/oclif/commands/team/remove.js
new file mode 100644
index 000000000..8af379ac9
--- /dev/null
+++ b/packages/cli/src/oclif/commands/team/remove.js
@@ -0,0 +1,77 @@
+const ZapierBaseCommand = require('../../ZapierBaseCommand');
+const { cyan } = require('colors/safe');
+const { buildFlags } = require('../../buildFlags');
+const {
+ callAPI,
+ getLinkedApp,
+ listEndpointMulti
+} = require('../../../utils/api');
+const { BASE_ENDPOINT } = require('../../../constants');
+
+const roleName = role => (role === 'collaborator' ? 'admin' : 'subscriber');
+
+class TeamRemoveCommand extends ZapierBaseCommand {
+ async perform() {
+ this.startSpinner('Loading team members');
+ const { admins, subscribers } = await listEndpointMulti(
+ { endpoint: 'collaborators', keyOverride: 'admins' },
+ {
+ endpoint: app =>
+ `${BASE_ENDPOINT}/api/platform/v3/integrations/${app.id}/subscribers`,
+ keyOverride: 'subscribers'
+ }
+ );
+
+ const choices = [...admins, ...subscribers].map(
+ ({ status, name, role, email, id }) => ({
+ status,
+ value: { id, email, role: roleName(role) },
+ name: `${email} (${roleName(role)})`,
+ short: email
+ })
+ );
+
+ this.stopSpinner();
+
+ const { role, email, id: invitationId } = await this.promptWithList(
+ 'Which team member do you want to remove?',
+ choices
+ );
+ this.log();
+ if (
+ !(await this.confirm(
+ `About to revoke ${cyan(role)}-level access from ${cyan(
+ email
+ )}. Are you sure?`,
+ true
+ ))
+ ) {
+ this.log('\ncancelled');
+ return;
+ }
+
+ const roleIsAdmin = role === 'admin';
+
+ this.startSpinner('Removing Team Member');
+ const { id: appId } = await getLinkedApp();
+ const url = roleIsAdmin
+ ? `/apps/${appId}/collaborators/${invitationId}`
+ : `${BASE_ENDPOINT}/api/platform/v3/integrations/${appId}/subscribers/${invitationId}`;
+
+ await callAPI(url, {
+ url: url.startsWith('http') ? url : undefined,
+ method: 'DELETE',
+ body: { email: this.args.email }
+ });
+
+ this.stopSpinner();
+ }
+}
+
+TeamRemoveCommand.flags = buildFlags();
+TeamRemoveCommand.description = `Remove a team member from all versions of your integration.
+
+Admins will immediately lose write access to the app. Subscribers won't receive future email updates.`;
+TeamRemoveCommand.aliases = ['team:delete'];
+
+module.exports = TeamRemoveCommand;
diff --git a/packages/cli/src/oclif/commands/users/add.js b/packages/cli/src/oclif/commands/users/add.js
new file mode 100644
index 000000000..5fd937387
--- /dev/null
+++ b/packages/cli/src/oclif/commands/users/add.js
@@ -0,0 +1,62 @@
+const ZapierBaseCommand = require('../../ZapierBaseCommand');
+const { flags } = require('@oclif/command');
+const { cyan } = require('colors/safe');
+const { buildFlags } = require('../../buildFlags');
+const { callAPI, getLinkedApp } = require('../../../utils/api');
+
+class UsersAddCommand extends ZapierBaseCommand {
+ async perform() {
+ if (
+ !this.flags.force &&
+ !(await this.confirm(
+ `About to invite ${cyan(this.args.email)} to ${
+ this.args.version ? `version ${this.args.version}` : 'all versions'
+ } of your integration. An invite email will be sent. Is that ok?`,
+ true
+ ))
+ ) {
+ this.log('\ncancelled');
+ return;
+ }
+
+ this.startSpinner('Inviting User');
+ const { id } = await getLinkedApp();
+ const url = `/apps/${id}/invitees/${this.args.email}${
+ this.args.version ? `/${this.args.version}` : ''
+ }`;
+ await callAPI(url, { method: 'POST' });
+ this.stopSpinner();
+ }
+}
+
+UsersAddCommand.args = [
+ {
+ name: 'email',
+ description:
+ "The user to be invited. If they don't have a Zapier account, they'll be prompted to create one.",
+ required: true
+ },
+ {
+ name: 'version',
+ description:
+ 'A version string (like 1.2.3). Optional, used only if you want to invite a user to a specific version instead of all versions.'
+ }
+];
+UsersAddCommand.flags = buildFlags({
+ commandFlags: {
+ force: flags.boolean({
+ char: 'f',
+ description: 'Skip confirmation. Useful for running programatically.'
+ })
+ }
+});
+UsersAddCommand.description = `Add a user to some or all versions of your integration.
+
+When this command is run, we'll send an email to the user inviting them to try your app. You can track the status of that invite using the \`${cyan(
+ 'zapier users:get'
+)}\` command.
+
+Invited users will be able to see your integration's name, logo, and description. They'll also be able to create Zaps using any available triggers and actions.`;
+UsersAddCommand.aliases = ['users:invite'];
+
+module.exports = UsersAddCommand;
diff --git a/packages/cli/src/oclif/commands/users/get.js b/packages/cli/src/oclif/commands/users/get.js
new file mode 100644
index 000000000..1f896c910
--- /dev/null
+++ b/packages/cli/src/oclif/commands/users/get.js
@@ -0,0 +1,54 @@
+const ZapierBaseCommand = require('../../ZapierBaseCommand');
+const { cyan, yellow } = require('colors/safe');
+const { listEndpoint } = require('../../../utils/api');
+const { buildFlags } = require('../../buildFlags');
+
+class UsersListCommand extends ZapierBaseCommand {
+ async perform() {
+ this.startSpinner('Loading users');
+ const { users } = await listEndpoint('invitees', 'users');
+
+ const cleanedUsers = users.map(u => ({
+ ...u,
+ app_version: u.app_version || 'All'
+ }));
+
+ this.stopSpinner();
+
+ this.log(
+ `\n${yellow(
+ 'Note'
+ )} that this list of users is NOT a comprehensive list of everyone who is using your integration. It only includes users who were invited directly by email (using the \`users:add EMAIL\` command or the web UI).\n`
+ );
+
+ this.logTable({
+ rows: cleanedUsers,
+ headers: [
+ ['Email', 'email'],
+ ['Status', 'status'],
+ ['Version', 'app_version']
+ ],
+ emptyMessage: 'No users have been invited directly by email.'
+ });
+
+ this.log(
+ `\nTo invite users via a link, use the \`${cyan(
+ 'zapier users:links'
+ )}\` command. To invite a specific user by email, use the \`${cyan(
+ 'zapier users:add'
+ )}\` command.`
+ );
+ }
+}
+
+UsersListCommand.flags = buildFlags({ opts: { format: true } });
+UsersListCommand.description = `Get a list of users who have been invited to your app.
+
+Note that this list of users is NOT a comprehensive list of everyone who is using your integration. It only includes users who were invited directly by email (using the \`${cyan(
+ 'zapier users:add'
+)}\` command or the web UI). Users who joined by clicking links generated using the \`${cyan(
+ 'zapier user:links'
+)}\` command won't show up here.`;
+UsersListCommand.aliases = ['users:list'];
+
+module.exports = UsersListCommand;
diff --git a/packages/cli/src/oclif/commands/users/links.js b/packages/cli/src/oclif/commands/users/links.js
new file mode 100644
index 000000000..bc981d5c3
--- /dev/null
+++ b/packages/cli/src/oclif/commands/users/links.js
@@ -0,0 +1,43 @@
+const ZapierBaseCommand = require('../../ZapierBaseCommand');
+const { bold, cyan } = require('colors/safe');
+const { listEndpoint } = require('../../../utils/api');
+const { buildFlags } = require('../../buildFlags');
+
+class UsersLinksCommand extends ZapierBaseCommand {
+ async perform() {
+ this.startSpinner('Loading links');
+ const {
+ invite_url: inviteUrl,
+ versions_invite_urls: versionInviteUrls
+ } = await listEndpoint('invitees');
+
+ this.stopSpinner();
+
+ this.log(
+ `\nYou can invite users to ${bold(
+ 'all'
+ )} versions of your app using the following link:`
+ );
+ this.log(`\n\n${cyan(inviteUrl)}\n\n`);
+
+ this.log(
+ 'You can invite users to specific app versions using the following links:\n'
+ );
+ this.logTable({
+ rows: Object.entries(versionInviteUrls).map(([version, url]) => ({
+ version,
+ url
+ })),
+ headers: [['Version', 'version'], ['URL', 'url']]
+ });
+
+ this.log(
+ '\nTo invite a specific user by email, use the `zapier users:add` command.'
+ );
+ }
+}
+
+UsersLinksCommand.flags = buildFlags({ opts: { format: true } });
+UsersLinksCommand.description = `Get a list of links that are used to invite users to your app.`;
+
+module.exports = UsersLinksCommand;
diff --git a/packages/cli/src/oclif/commands/users/remove.js b/packages/cli/src/oclif/commands/users/remove.js
new file mode 100644
index 000000000..a3171eb03
--- /dev/null
+++ b/packages/cli/src/oclif/commands/users/remove.js
@@ -0,0 +1,50 @@
+const ZapierBaseCommand = require('../../ZapierBaseCommand');
+const { flags } = require('@oclif/command');
+const { cyan } = require('colors/safe');
+const { buildFlags } = require('../../buildFlags');
+const { callAPI, getLinkedApp } = require('../../../utils/api');
+
+class UsersRemoveCommand extends ZapierBaseCommand {
+ async perform() {
+ if (
+ !this.flags.force &&
+ !(await this.confirm(
+ `About to revoke access to ${cyan(
+ this.args.email
+ )}. They won't be able to see your app in the editor and their Zaps will stop working. Are you sure?`,
+ true
+ ))
+ ) {
+ this.log('\ncancelled');
+ return;
+ }
+
+ this.startSpinner('Removing User');
+ const { id } = await getLinkedApp();
+ const url = `/apps/${id}/invitees/${this.args.email}`;
+ await callAPI(url, { method: 'DELETE' });
+ this.stopSpinner();
+ }
+}
+
+UsersRemoveCommand.args = [
+ {
+ name: 'email',
+ description: 'The user to be removed.',
+ required: true
+ }
+];
+UsersRemoveCommand.flags = buildFlags({
+ commandFlags: {
+ force: flags.boolean({
+ char: 'f',
+ description: 'Skip confirmation. Useful for running programatically.'
+ })
+ }
+});
+UsersRemoveCommand.description = `Remove a user from all versions of your integration.
+
+When this command is run, their Zaps will immediately turn off. They won't be able to use your app again until they're re-invited or it has gone public. In practice, this command isn't run often as it's very disruptive to users.`;
+UsersRemoveCommand.aliases = ['users:delete'];
+
+module.exports = UsersRemoveCommand;
diff --git a/packages/cli/src/oclif/oCommands.js b/packages/cli/src/oclif/oCommands.js
index 806694129..c443076b3 100644
--- a/packages/cli/src/oclif/oCommands.js
+++ b/packages/cli/src/oclif/oCommands.js
@@ -5,7 +5,7 @@ module.exports = {
apps: true,
build: require('./commands/build'),
deprecate: require('./commands/deprecate'),
- env: true, // used so that subcommands are properly routed into ocli, but `env` itself doesn't show in help/docs
+ env: true, // used so that aliases are properly routed into ocli, but `env` itself doesn't show in help/docs
'env:get': require('./commands/env/get'),
'env:set': require('./commands/env/set'),
'env:unset': require('./commands/env/unset'),
@@ -18,8 +18,17 @@ module.exports = {
promote: require('./commands/promote'),
push: require('./commands/push'),
scaffold: require('./commands/scaffold'),
+ team: true,
+ 'team:add': require('./commands/team/add'),
+ 'team:get': require('./commands/team/get'),
+ 'team:remove': require('./commands/team/remove'),
test: require('./commands/test'),
upload: require('./commands/upload'),
+ users: true,
+ 'users:add': require('./commands/users/add'),
+ 'users:get': require('./commands/users/get'),
+ 'users:links': require('./commands/users/links'),
+ 'users:remove': require('./commands/users/remove'),
validate: require('./commands/validate'),
versions: require('./commands/versions')
};
diff --git a/packages/cli/src/utils/api.js b/packages/cli/src/utils/api.js
index 00c9f4f9e..fdc615643 100644
--- a/packages/cli/src/utils/api.js
+++ b/packages/cli/src/utils/api.js
@@ -249,18 +249,34 @@ const listApps = async () => {
};
};
-const listEndpoint = (endpoint, keyOverride) => {
- return checkCredentials()
- .then(() => getLinkedApp())
- .then(app => {
- return Promise.all([app, callAPI(`/apps/${app.id}/${endpoint}`)]);
- })
- .then(([app, results]) => {
- const out = { app };
- out[keyOverride || endpoint] = results.objects;
- _.assign(out, _.omit(results, 'objects'));
- return out;
+// endpoint can be string or func(app)
+const listEndpoint = async (endpoint, keyOverride) =>
+ listEndpointMulti({ endpoint, keyOverride });
+
+// takes {endpoint: string, keyOverride?: string}[]
+const listEndpointMulti = async (...calls) => {
+ await checkCredentials();
+ const app = await getLinkedApp();
+ let output = { app };
+
+ for (const { endpoint, keyOverride } of calls) {
+ if ((_.isFunction(endpoint) || endpoint.includes('/')) && !keyOverride) {
+ throw new Error('must incude keyOverride with complex endpoint');
+ }
+
+ const route = _.isFunction(endpoint)
+ ? endpoint(app)
+ : `/apps/${app.id}/${endpoint}`;
+
+ const results = await callAPI(route, {
+ // if a full url comes out of the function, we have to use that
+ url: route.startsWith('http') ? route : undefined
});
+
+ const { objects, ...theRest } = results;
+ output = { ...output, [keyOverride || endpoint]: objects, ...theRest };
+ }
+ return output;
};
const listVersions = () => listEndpoint('versions');
@@ -352,6 +368,7 @@ module.exports = {
getVersionInfo,
listApps,
listEndpoint,
+ listEndpointMulti,
listEnv,
listHistory,
listInvitees,