From 33b8e298e6d316e90c66a476be37f935a54dfee6 Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Mon, 29 Nov 2021 18:48:59 +0100 Subject: [PATCH 1/4] Add command to delete Apps Script projects --- src/auth.ts | 2 ++ src/commands/delete.ts | 63 +++++++++++++++++++++++++++++++++++++++++ src/index.ts | 16 +++++++++++ src/inquirer.ts | 29 +++++++++++++++++++ src/messages.ts | 5 ++++ src/utils.ts | 5 ++++ test/commands/delete.ts | 62 ++++++++++++++++++++++++++++++++++++++++ test/test.ts | 2 ++ 8 files changed, 184 insertions(+) create mode 100644 src/commands/delete.ts create mode 100644 test/commands/delete.ts diff --git a/src/auth.ts b/src/auth.ts index 1765bd2f..0c095909 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -60,6 +60,7 @@ let localOAuth2Client: OAuth2Client; // Must be set up after authorize. export const discovery = google.discovery({version: 'v1'}); export const drive = google.drive({version: 'v3', auth: globalOAuth2Client}); +export const driveV2 = google.drive({version: 'v2', auth: globalOAuth2Client}); export const logger = google.logging({version: 'v2', auth: globalOAuth2Client}); export const script = google.script({version: 'v1', auth: globalOAuth2Client}); export const serviceUsage = google.serviceusage({version: 'v1', auth: globalOAuth2Client}); @@ -78,6 +79,7 @@ export const defaultScopes = [ 'https://www.googleapis.com/auth/script.projects', // Apps Script management scopeWebAppDeploy, // Apps Script Web Apps 'https://www.googleapis.com/auth/drive.metadata.readonly', // Drive metadata + 'https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/drive.file', // Create Drive files 'https://www.googleapis.com/auth/service.management', // Cloud Project Service Management API 'https://www.googleapis.com/auth/logging.read', // StackDriver logs diff --git a/src/commands/delete.ts b/src/commands/delete.ts new file mode 100644 index 00000000..9d830366 --- /dev/null +++ b/src/commands/delete.ts @@ -0,0 +1,63 @@ +import {driveV2, loadAPICredentials} from '../auth.js'; +import {deleteClaspJsonPrompt, deleteDriveFilesPrompt} from '../inquirer.js'; +import {LOG} from '../messages.js'; +import {checkIfOnlineOrDie, deleteProject, getProjectSettings, spinner, stopSpinner} from '../utils.js'; + +interface CommandOption { + readonly force?: boolean; +} + +/** + * Delete an Apps Script project. + * @param options.foce {boolean} force Bypass any confirmation messages. + */ +export default async (options: CommandOption): Promise => { + // Handle common errors. + await checkIfOnlineOrDie(); + await loadAPICredentials(); + + // Create defaults. + const {force} = options; + + const projectSettings = await getProjectSettings(); + const parentIds = projectSettings.parentId || []; + const hasParents = !!parentIds.length; + + //ask confirmation + if (!force && !(await deleteDriveFilesPrompt(hasParents)).answer) { + return; + } + + //delete the drive files + if (hasParents) { + await deleteDriveFiles(parentIds); + } else { + await deleteDriveFiles([projectSettings.scriptId]); + } + + // TODO: delete .clasp.json // + if (force || (await deleteClaspJsonPrompt()).answer) { + await deleteProject(); + } + + console.log(LOG.DELETE_DRIVE_FILE_FINISH); +}; + +/** + * Delete Files on Drive. + * + * @param {string[]} fileIds the list of ids + */ +const deleteDriveFiles = async (fileIds: string[]): Promise => { + for (let i = 0; i < fileIds.length; i++) { + const currId = fileIds[i]; + + // CLI Spinner + spinner.start(LOG.DELETE_DRIVE_FILE_START(currId)); + + // Delete Apps Script project + await driveV2.files.trash({fileId: currId}); + } + + stopSpinner(); +}; diff --git a/src/index.ts b/src/index.ts index 891fe4f5..b80c4eeb 100755 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ import {ClaspError} from './clasp-error.js'; import apis from './commands/apis.js'; import clone from './commands/clone.js'; import create from './commands/create.js'; +import deleteCmd from './commands/delete.js'; import defaultCmd from './commands/default.js'; import deploy from './commands/deploy.js'; import deployments from './commands/deployments.js'; @@ -158,6 +159,21 @@ program .option('--rootDir ', 'Local root directory in which clasp will store your project files.') .action(create); +/** + * Delete a clasp project + * @name delete + * @param {boolean?} force Bypass any confirmation messages. It’s not a good idea to do this unless you want to run clasp from a script. + * @example delete + */ +program + .command('delete') + .description('Delete a project') + .option( + '-f, --force', + 'Bypass any confirmation messages. It’s not a good idea to do this unless you want to run clasp from a script.' + ) + .action(deleteCmd); + /** * Fetches a project and saves the script id locally. * @param {string?} [scriptId] The script ID to clone. diff --git a/src/inquirer.ts b/src/inquirer.ts index bf8023be..fc3c0e57 100644 --- a/src/inquirer.ts +++ b/src/inquirer.ts @@ -148,3 +148,32 @@ export const scriptTypePrompt = () => type: 'list', }, ]); + +/** + * Inquirer prompt for deleting a drive file + * @param {boolean} withParent true if the script has a parent project, default is false + * @returns {Promise<{ answer: boolean }>} A promise for an object with the `answer` property. + */ +export const deleteDriveFilesPrompt = (withParent = false) => + prompt<{answer: boolean}>([ + { + default: false, + message: !withParent ? LOG.DELETE_DRIVE_FILE_CONFIRM : LOG.DELETE_DRIVE_FILE_WITH_PARENT_CONFIRM, + name: 'answer', + type: 'confirm', + }, + ]); + +/** + * Inquirer prompt for deleting the .clasp.json file + * @returns {Promise<{ answer: boolean }>} A promise for an object with the `answer` property. + */ +export const deleteClaspJsonPrompt = () => + prompt<{answer: boolean}>([ + { + default: false, + message: LOG.DELETE_CLASPJSON_CONFIRM, + name: 'answer', + type: 'confirm', + }, + ]); diff --git a/src/messages.ts b/src/messages.ts index 2466bc8c..30b1c311 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -115,6 +115,11 @@ Cloned ${fileCount} ${fileCount === 1 ? 'file' : 'files'}.`, CREATE_PROJECT_FINISH: (filetype: string, scriptId: string) => `Created new ${getScriptTypeName(filetype)} script: ${URL.SCRIPT(scriptId)}`, CREATE_PROJECT_START: (title: string) => `Creating new script: ${title}…`, + DELETE_DRIVE_FILE_START: (fileId: string) => `Deleting ${fileId}…`, + DELETE_DRIVE_FILE_FINISH: `Deleted project`, + DELETE_DRIVE_FILE_CONFIRM: 'Are you sure you want to delete the script?', + DELETE_DRIVE_FILE_WITH_PARENT_CONFIRM: 'Are you sure you want to delete the script and his parent projects?', + DELETE_CLASPJSON_CONFIRM: 'Do you want to delete the .clasp.json file?', CREDENTIALS_FOUND: 'Credentials found, using those to login…', CREDS_FROM_PROJECT: (projectId: string) => `Using credentials located here:\n${URL.CREDS(projectId)}\n`, DEFAULT_CREDENTIALS: 'No credentials given, continuing with default…', diff --git a/src/utils.ts b/src/utils.ts index 35677353..381793e7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -233,6 +233,11 @@ export const checkIfOnlineOrDie = async () => { export const saveProject = async (projectSettings: ProjectSettings, append = true): Promise => DOTFILE.PROJECT().write(append ? {...(await getProjectSettings()), ...projectSettings} : projectSettings); +/** + * Deletes the project dotfile. + */ +export const deleteProject = async (): Promise => await DOTFILE.PROJECT().delete(); + /** * Gets the script's Cloud Platform Project Id from project settings file or prompt for one. * @returns {Promise} A promise to get the projectId string. diff --git a/test/commands/delete.ts b/test/commands/delete.ts new file mode 100644 index 00000000..c88afddd --- /dev/null +++ b/test/commands/delete.ts @@ -0,0 +1,62 @@ +import fs from 'fs-extra'; +import {expect} from 'chai'; +import {spawnSync} from 'child_process'; +import {before, describe, it} from 'mocha'; +import {Conf} from '../../src/conf.js'; + +// import {LOG} from '../../src/messages.js'; +import {CLASP} from '../constants.js'; +import {cleanup, setup} from '../functions.js'; +import {LOG} from '../../src/messages.js'; + +const config = Conf.get(); + +describe('Test clasp delete function with standalone script', () => { + before(setup); + it('should create a new project correctly before delete', () => { + spawnSync('rm', ['.clasp.json']); + const result = spawnSync(CLASP, ['create', '--type', 'standalone', '--title', 'myTitleToDelete'], { + encoding: 'utf8', + }); + expect(result.status).to.equal(0); + }); + + it('should delete the new project correctly', () => { + const result = spawnSync(CLASP, ['delete', '-f'], { + encoding: 'utf8', + }); + expect(result.stdout).to.contains(LOG.DELETE_DRIVE_FILE_FINISH); + expect(fs.existsSync(config.projectConfig!)).to.be.false; + expect(result.status).to.equal(0); + }); + after(cleanup); +}); + +describe('Test clasp delete function with a parent project', () => { + before(setup); + it('should create a new project correctly before delete', () => { + spawnSync('rm', ['.clasp.json']); + const result = spawnSync(CLASP, ['create', '--type', 'sheets', '--title', 'myTitleToDelete'], { + encoding: 'utf8', + }); + expect(result.status).to.equal(0); + }); + + it('should ask if delete the parent project', () => { + const result = spawnSync(CLASP, ['delete'], { + encoding: 'utf8', + }); + expect(result.stdout).to.contain(LOG.DELETE_DRIVE_FILE_WITH_PARENT_CONFIRM); + expect(result.status).to.equal(0); + }); + + it('should delete the new project correctly', () => { + const result = spawnSync(CLASP, ['delete', '-f'], { + encoding: 'utf8', + }); + expect(result.stdout).to.contains(LOG.DELETE_DRIVE_FILE_FINISH); + expect(fs.existsSync(config.projectConfig!)).to.be.false; + expect(result.status).to.equal(0); + }); + after(cleanup); +}); diff --git a/test/test.ts b/test/test.ts index a6f7e135..f7c7fbe1 100644 --- a/test/test.ts +++ b/test/test.ts @@ -28,6 +28,7 @@ describe.skip('Test --help for each function', () => { it('should logout --help', () => expectHelp('logout', 'Log out')); it('should create --help', () => expectHelp('create', 'Create a script')); it('should clone --help', () => expectHelp('clone', 'Clone a project')); + it('should delete --help', () => expectHelp('delete', 'Delete a project')); it('should pull --help', () => expectHelp('pull', 'Fetch a remote project')); it('should push --help', () => expectHelp('push', 'Update the remote project')); it('should status --help', () => expectHelp('status', 'Lists files that will be pushed by clasp')); @@ -266,6 +267,7 @@ describe('Test all functions while logged out', () => { }; it('should fail to list (no credentials)', () => expectNoCredentials('list')); it('should fail to clone (no credentials)', () => expectNoCredentials('clone')); + it('should fail to delete (no credentials)', () => expectNoCredentials('delete')); it('should fail to push (no credentials)', () => expectNoCredentials('push')); it('should fail to deployments (no credentials)', () => expectNoCredentials('deployments')); it('should fail to deploy (no credentials)', () => expectNoCredentials('deploy')); From 42efaffe9174f2888648a3b8f9bf175330c1d767 Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Mon, 29 Nov 2021 19:29:49 +0100 Subject: [PATCH 2/4] Changed `` quotes to '' to fix linting warning --- src/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/messages.ts b/src/messages.ts index 30b1c311..02f8b2b9 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -116,7 +116,7 @@ Cloned ${fileCount} ${fileCount === 1 ? 'file' : 'files'}.`, `Created new ${getScriptTypeName(filetype)} script: ${URL.SCRIPT(scriptId)}`, CREATE_PROJECT_START: (title: string) => `Creating new script: ${title}…`, DELETE_DRIVE_FILE_START: (fileId: string) => `Deleting ${fileId}…`, - DELETE_DRIVE_FILE_FINISH: `Deleted project`, + DELETE_DRIVE_FILE_FINISH: 'Deleted project', DELETE_DRIVE_FILE_CONFIRM: 'Are you sure you want to delete the script?', DELETE_DRIVE_FILE_WITH_PARENT_CONFIRM: 'Are you sure you want to delete the script and his parent projects?', DELETE_CLASPJSON_CONFIRM: 'Do you want to delete the .clasp.json file?', From 8b589999303da69f00d67e9bb64192f9f0b90358 Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Tue, 30 Nov 2021 09:10:26 +0100 Subject: [PATCH 3/4] Updated README.md with the delete command instructions --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 2bc81d3c..e17ee75d 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ clasp - [`clasp logout`](#logout) - [`clasp create [--title ] [--type <type>] [--rootDir <dir>] [--parentId <id>]`](#create) - [`clasp clone <scriptId | scriptURL> [versionNumber] [--rootDir <dir>]`](#clone) +- [`clasp delete [--force]`](#delete) - [`clasp pull [--versionNumber]`](#pull) - [`clasp push [--watch] [--force]`](#push) - [`clasp status [--json]`](#status) @@ -194,6 +195,19 @@ Clones the script project from script.google.com. - `clasp clone "https://script.google.com/d/15ImUCpyi1Jsd8yF8Z6wey_7cw793CymWTLxOqwMka3P1CzE5hQun6qiC/edit"` - `clasp clone "15ImUCpyi1Jsd8yF8Z6wey_7cw793CymWTLxOqwMka3P1CzE5hQun6qiC" --rootDir ./src` +### Delete + +Interactively deletes a script or a project and the `.clasp.json` file. Prompt the user for confirmation if the --force option is not specified. + +#### Options + +- `-f` `--force`: Bypass any confirmation messages. It’s not a good idea to do this unless you want to run clasp from a script. + +#### Examples + +- `clasp delete +- `clasp delete -f + ### Pull Fetches a project from either a provided or saved script ID. From 2c85f16320238026e820906ed7765af4835d5603 Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli <fabrizio.antonangeli@gmail.com> Date: Tue, 30 Nov 2021 09:17:06 +0100 Subject: [PATCH 4/4] Updated README.md with delete command instructions --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e17ee75d..545156fb 100644 --- a/README.md +++ b/README.md @@ -205,8 +205,8 @@ Interactively deletes a script or a project and the `.clasp.json` file. Prompt t #### Examples -- `clasp delete -- `clasp delete -f +- `clasp delete` +- `clasp delete -f` ### Pull