UNPKG

@google/clasp

Version:

Develop Apps Script Projects locally

97 lines (96 loc) 5.37 kB
// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // This file defines the 'pull' command for the clasp CLI. import { Command } from 'commander'; import fs from 'fs/promises'; import inquirer from 'inquirer'; import { intl } from '../intl.js'; import { isInteractive, withSpinner } from './utils.js'; export const command = new Command('pull') .description('Fetch a remote project') .option('--versionNumber <version>', 'The version number of the project to retrieve.') .option('-d, --deleteUnusedFiles ', 'Delete local files that are not in the remote project. Use with caution.') .option('-f, --force', 'Forcibly delete local files that are not in the remote project without prompting.') .action(async function () { const options = this.optsWithGlobals(); const clasp = options.clasp; const versionNumber = options.versionNumber; const forceDelete = options.force; // First, collect a list of current local files before pulling. // This is used to determine which files might need to be deleted if --deleteUnusedFiles is active. let spinnerMsg = intl.formatMessage({ id: "dh7Bw6", defaultMessage: [{ type: 0, value: "Checking local files..." }] }); const localFiles = await clasp.files.collectLocalFiles(); // Perform the pull operation from the remote Apps Script project. // This fetches the files (optionally a specific version) and writes them to the local filesystem. spinnerMsg = intl.formatMessage({ id: "jilcJH", defaultMessage: [{ type: 0, value: "Pulling files..." }] }); const files = await withSpinner(spinnerMsg, async () => { return clasp.files.pull(versionNumber); // `clasp.files.pull` handles fetching and writing. }); const pulledFiles = files.map(f => f.localPath); let deletedFiles = []; // If the --deleteUnusedFiles option is used, identify and delete local files // that are no longer present in the remote project. if (options.deleteUnusedFiles) { // Compare the initial list of local files with the files just pulled. // Any file in `localFiles` that is not in `files` (the pulled files) is considered unused. const filesToDelete = localFiles.filter(f => !files.find(p => p.localPath === f.localPath)); deletedFiles = await deleteLocalFiles(filesToDelete, forceDelete, options.json); } if (options.json) { console.log(JSON.stringify({ pulledFiles, deletedFiles }, null, 2)); return; } // Log the paths of the pulled files. files.forEach(f => console.log(`└─ ${f.localPath}`)); const successMessage = intl.formatMessage({ id: "4mRAfN", defaultMessage: [{ type: 0, value: "Pulled " }, { type: 6, value: "count", options: { "=0": { value: [{ type: 0, value: "no files." }] }, one: { value: [{ type: 0, value: "one file." }] }, other: { value: [{ type: 7 }, { type: 0, value: " files" }] } }, offset: 0, pluralType: "cardinal" }, { type: 0, value: "." }] }, { count: files.length, }); console.log(successMessage); }); async function deleteLocalFiles(filesToDelete, forceDelete = false, json = false) { if (!filesToDelete || filesToDelete.length === 0) { return []; // No files to delete. } const skipConfirmation = forceDelete; // If not in an interactive terminal and --force is not used, skip deletion with a warning. // This prevents accidental deletion in non-interactive environments like CI scripts. if (!isInteractive() && !forceDelete) { if (!json) { const msg = intl.formatMessage({ id: "zLuvSg", defaultMessage: [{ type: 0, value: "You are not in an interactive terminal and --force not used. Skipping file deletion." }] }); console.warn(msg); } return []; } const deletedFiles = []; for (const file of filesToDelete) { let doDelete = true; // Assume deletion unless confirmation is required and denied. if (!skipConfirmation) { // If not forcing, prompt the user to confirm deletion for each file. const confirm = await inquirer.prompt({ type: 'confirm', name: 'deleteFile', message: intl.formatMessage({ id: "lVx/lI", defaultMessage: [{ type: 0, value: "Delete " }, { type: 1, value: "file" }, { type: 0, value: "?" }] }, { file: file.localPath }), }); doDelete = confirm.deleteFile; } if (doDelete) { await fs.unlink(file.localPath); // Delete the file from the local system. deletedFiles.push(file.localPath); if (!json) { console.log(intl.formatMessage({ id: "Nx315v", defaultMessage: [{ type: 0, value: "Deleted " }, { type: 1, value: "file" }] }, { file: file.localPath })); } } } return deletedFiles; }