diff --git a/README.md b/README.md index cc94912d..88355ffa 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ clasp - [`clasp pull [--versionNumber]`](#pull) - [`clasp push [--watch] [--force]`](#push) - [`clasp status [--json]`](#status) +- [`clasp open [scriptId] [--webapp] [--creds] [--account ]`](#open) - [`clasp open [scriptId] [--webapp] [--creds] [--addon]`](#open) - [`clasp deployments`](#deployments) - [`clasp deploy [--versionNumber ] [--description ] [--deploymentId ]`](#deploy) @@ -258,6 +259,7 @@ Opens the current directory's `clasp` project on script.google.com. Provide a `s - `[scriptId]`: The optional script project to open. - `--webapp`: Open web application in a browser. - `--creds`: Open the URL to create credentials. +- `--account `: Open script using specific email or Google account number. - `--addon`: List parent IDs and open the URL of the first one. #### Examples @@ -266,6 +268,8 @@ Opens the current directory's `clasp` project on script.google.com. Provide a `s - `clasp open "15ImUCpyi1Jsd8yF8Z6wey_7cw793CymWTLxOqwMka3P1CzE5hQun6qiC"` - `clasp open --webapp` - `clasp open --creds` +- `clasp open --account user@example.com` +- `clasp open --account 1` - `clasp open --addon` ### Deployments diff --git a/src/commands/openCmd.ts b/src/commands/openCmd.ts index 29e7de2b..4867708a 100644 --- a/src/commands/openCmd.ts +++ b/src/commands/openCmd.ts @@ -3,7 +3,7 @@ import open from 'open'; import { loadAPICredentials, script } from '../auth'; import { deploymentIdPrompt } from '../inquirer'; import { URL } from '../urls'; -import { ERROR, getProjectSettings, getWebApplicationURL, LOG, logError } from '../utils'; +import { ERROR, getProjectSettings, getWebApplicationURL, LOG, logError, isValidEmail } from '../utils'; interface EllipizeOptions { ellipse?: string; @@ -17,12 +17,14 @@ import ellipsize from 'ellipsize'; * @param scriptId {string} The Apps Script project to open. * @param cmd.webapp {boolean} If true, the command will open the webapps URL. * @param cmd.creds {boolean} If true, the command will open the credentials URL. + * @param cmd.account {string} Email or user number authenticate with when opening */ export default async ( scriptId: string, cmd: { webapp: boolean; creds: boolean; + account: string; addon: boolean; }, ): Promise => { @@ -59,9 +61,20 @@ export default async ( // If we're not a web app, open the script URL. if (!cmd.webapp) { - console.log(LOG.OPEN_PROJECT(scriptId)); - await open(URL.SCRIPT(scriptId)); - return; + // If we should open script with a specific account + if (cmd.account) { + // Confirm account looks like an email address + if (cmd.account.length > 2 && !isValidEmail(cmd.account)) { + logError(null, ERROR.EMAIL_INCORRECT(cmd.account)); + } + // Check if account is number + if (cmd.account.length < 3 && isNaN(Number(cmd.account))) { + logError(null, ERROR.ACCOUNT_INCORRECT(cmd.account)); + } + } + + console.log(LOG.OPEN_PROJECT(scriptId, cmd.account)); + return open(URL.SCRIPT(scriptId, cmd.account), { wait: false }); } // Web app: Otherwise, open the latest deployment. diff --git a/src/index.ts b/src/index.ts index 755d1c52..60e19896 100755 --- a/src/index.ts +++ b/src/index.ts @@ -186,6 +186,7 @@ commander .description('Open a script') .option('--webapp', 'Open web application in the browser') .option('--creds', 'Open the URL to create credentials') + .option('--account ', 'Authenticate with specific email when opening') .option('--addon', 'List parent IDs and open the URL of the first one') .action(handleError(openCmd)); diff --git a/src/urls.ts b/src/urls.ts index 753d43c4..e2ed1ad8 100644 --- a/src/urls.ts +++ b/src/urls.ts @@ -30,6 +30,6 @@ export const URL = { `https://console.cloud.google.com/logs/viewer?project=${projectId}&resource=app_script_function`, SCRIPT_API_USER: 'https://script.google.com/home/usersettings', // It is too expensive to get the script URL from the Drive API. (Async/not offline) - SCRIPT: (scriptId: string) => `https://script.google.com/d/${scriptId}/edit`, + SCRIPT: (scriptId: string, account?: string) => `https://script.google.com/d/${scriptId}/edit${typeof account === 'undefined' ? '' : `?authuser=${account}`}`, DRIVE: (driveId: string) => `https://drive.google.com/open?id=${driveId}`, }; diff --git a/src/utils.ts b/src/utils.ts index f1c3ebd3..d0a3bcb2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -58,7 +58,9 @@ export function getOAuthSettings(local: boolean): Promise { // Error messages (some errors take required params) export const ERROR = { - ACCESS_TOKEN: 'Error retrieving access token: ', + ACCESS_TOKEN: `Error retrieving access token: `, + ACCOUNT_INCORRECT: (account: string) => `The account "${account}" looks incorrect. +Is this an email or user number?`, BAD_CREDENTIALS_FILE: 'Incorrect credentials file format.', BAD_REQUEST: (message: string) => `Error: ${message} Your credentials may be invalid. Try logging in again.`, @@ -69,9 +71,11 @@ Forgot ${PROJECT_NAME} commands? Get help:\n ${PROJECT_NAME} --help`, CREATE_WITH_PARENT: 'Did you provide the correct parentId?', CREATE: 'Error creating script.', CREDENTIALS_DNE: (filename: string) => `Credentials file "${filename}" not found.`, - DEPLOYMENT_COUNT: 'Unable to deploy; Scripts may only have up to 20 versioned deployments at a time.', - DRIVE: 'Something went wrong with the Google Drive API', - EXECUTE_ENTITY_NOT_FOUND: 'Script API executable not published/deployed.', + DEPLOYMENT_COUNT: `Unable to deploy; Scripts may only have up to 20 versioned deployments at a time.`, + DRIVE: `Something went wrong with the Google Drive API`, + EMAIL_INCORRECT: (email: string) => `The email address "${email}" syntax looks incorrect. +There may be typos, did you provide a valid email?`, + EXECUTE_ENTITY_NOT_FOUND: `Script API executable not published/deployed.`, FOLDER_EXISTS: `Project file (${DOT.PROJECT.PATH}) already exists.`, FS_DIR_WRITE: 'Could not create directory.', FS_FILE_WRITE: 'Could not write file.', @@ -165,7 +169,7 @@ Cloned ${fileNum} ${fileNum === 1 ? 'file' : 'files'}.`, NO_GCLOUD_PROJECT: `No projectId found. Running ${PROJECT_NAME} logs --setup.`, OPEN_CREDS: (projectId: string) => `Opening credentials page: ${URL.CREDS(projectId)}`, OPEN_LINK: (link: string) => `Open this link: ${link}`, - OPEN_PROJECT: (scriptId: string) => `Opening script: ${URL.SCRIPT(scriptId)}`, + OPEN_PROJECT: (scriptId: string, account?: string) => `Opening script: ${URL.SCRIPT(scriptId, account)}`, OPEN_WEBAPP: (deploymentId?: string) => `Opening web application: ${deploymentId}`, OPEN_FIRST_PARENT: (parentId: string) => `Opening first parent: ${URL.DRIVE(parentId)}`, FOUND_PARENT: (parentId: string) => `Found parent: ${URL.DRIVE(parentId)}`, @@ -426,6 +430,15 @@ export function isValidProjectId(projectId: string) { return /^[a-z][-\da-z]{5,29}$/.test(projectId); } +/** + * Validate email address. + * @param {string} email The email address. + * @returns {boolean} Is the email address valid + */ +export function isValidEmail(email: string) { + return new RegExp(/^[^\s@]+@[^\s@]+\.[^\s@]+$/).test(email); +} + /** * Gets valid JSON obj or throws error. * @param str JSON string. diff --git a/tests/commands/open.ts b/tests/commands/open.ts index 362ccf56..bc02ea0c 100644 --- a/tests/commands/open.ts +++ b/tests/commands/open.ts @@ -33,6 +33,17 @@ describe('Test clasp open function', () => { ); expect(result.stdout).to.contain('Open which deployment?'); }); + it('should open script with account email correctly', () => { + const result = spawnSync( + CLASP, ['open', '--account', 'max@example.com'], { encoding: 'utf8' }, + ); + expect(result.stdout).to.contain('?authuser=max@example.com'); + }); + it('should open script with account number correctly', () => { + const result = spawnSync( + CLASP, ['open', '--account', '1'], { encoding: 'utf8' }, + ); + expect(result.stdout).to.contain('?authuser=1'); it('open parent page correctly', () => { const result = spawnSync( CLASP, ['open', '--addon'], { encoding: 'utf8' }, diff --git a/tests/utils.ts b/tests/utils.ts index 1f070ec2..c1c961b6 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -9,6 +9,7 @@ import { import { ERROR, getValidJSON, + isValidEmail, } from '../src/utils'; describe('Test getValidJSON function', () => { @@ -20,4 +21,19 @@ describe('Test getValidJSON function', () => { expect(() => getValidJSON(invalidExampleJSONString)).to.throw(ERROR.INVALID_JSON); }); after(cleanup); +}); + +describe('Test utils isValidEmail function', () => { + const validEmail = 'user@example.com'; + const invalidEmail = 'user@example'; + + // Disable a couple of linting rules just for these tests + // tslint:disable:no-unused-expression + it('should return true for valid combinations of input', () => { + expect(isValidEmail(validEmail)).to.be.true; + }); + + it('should return false for invalid combinations of input', () => { + expect(isValidEmail(invalidEmail)).to.be.false; + }); }); \ No newline at end of file