├── .babelrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── changelog.hbs ├── dist ├── index.js ├── prompts.js └── utils.js ├── package-lock.json ├── package.json └── src ├── index.js ├── prompts.js └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v4.1.0 (2023-11-22) 2 | 3 | * **chore** recreate fixed 4 | 5 | # v4.0.2 (2022-07-09) 6 | 7 | * **fix** team id append symbol 8 | * **chore** fix patch bump message 9 | 10 | # v4.0.1 (2022-07-09) 11 | 12 | * **fix** update deployment list endpoint 13 | 14 | # v4.0.0 (2022-07-09) 15 | 16 | * **chore** fix versioning scripts 17 | * **chore** update endpoints and read auth token from env 18 | * **chore** update endpoints and read auth token from env 19 | * **chore** update dependencies 20 | 21 | # v3.0.1 (2021-04-25) 22 | 23 | # v3.0.0 (2021-04-25) 24 | 25 | * **chore** add automatic changelog & version bump 26 | * **chore** init source from vercel package 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 source-from-vercel-deployment Cristian Calina 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Download source from Vercel deployment 2 | You can find the package on [npm](https://www.npmjs.com/package/source-from-vercel-deployment) 3 | 4 | ### Description 5 | 6 | A simple package made for downloading the source code from your Vercel Deployment. 7 | 8 | If you can't find your code anywhere but you have it deployed on Vercel, 9 | this package is for you. 10 | 11 | You can easily download your code from any deployment that you've had on the project. 12 | 13 | ### Installation 14 | 15 | 16 | To install it (not required if you use npx), simply run 17 | ``` 18 | npm install -g source-from-vercel-deployment 19 | ``` 20 | 21 | ### How to run the command 22 | 23 | After installing it globally, you can just type in CLI: 24 | ``` 25 | source-from-vercel-deployment 26 | ``` 27 | 28 | Or you can run it without installing using npx 29 | ``` 30 | npx source-from-vercel-deployment 31 | ``` 32 | 33 | You will then be prompted for the bellow mentioned values. 34 | 35 | ### Usage 36 | 37 | To run and download you're source code, you will be prompted for the following: 38 | 39 | * __AUTHORIZATION_TOKEN__: 40 | * You can generate one [here](https://vercel.com/account/tokens) 41 | * You can save the generated token in your Environmental Variables with the key **VERCEL_AUTH_TOKEN** so you won't be asked for it from the script. 42 | * __Choose Team__: 43 | * The team that you're project is on (or your personal account) 44 | * You can choose Personal project or select from the list of all teams that you are a part of. 45 | * __Choose Project Name__: 46 | * The project that contains the deployment that you whish to download the source for. 47 | * You can choose from all the projects from the selected team (or personal account) 48 | * __Choose Deployment__: 49 | * The deployment inside of the project that you whish to download the source for. 50 | * You can choose from all the deployments for the chosen project 51 | * __Choose OUTPUT_DIRECTORY__: 52 | * Where you want your deployment source to be placed 53 | * __Default value__: './deployment_source' 54 | 55 | 56 | ### Notes 57 | 58 | Don't overuse this package. Use it only if you really need it. It's calling the endpoint for every file to download it might take a while and it will do many requests to the api. 59 | 60 | If you get the error ```429``` with message ```TOO MANY REQUESTS```, it might mean that you've overused the package and made too many request. You have to wait a while until they reset. 61 | ( [more info here](https://vercel.com/docs/platform/limits#deployments-per-day-(free)) ) -------------------------------------------------------------------------------- /changelog.hbs: -------------------------------------------------------------------------------- 1 | {{#each releases}} 2 | # {{title}} ({{isoDate}}) 3 | {{#commit-list commits heading='' exclude='bump version'}} 4 | {{#if commit.breaking}}**BREAKING CHANGE:** {{/if}}{{subject}} 5 | {{/commit-list}} 6 | 7 | {{/each}} -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | var _axios = _interopRequireDefault(require("axios")); 5 | 6 | var _colors = _interopRequireDefault(require("colors")); 7 | 8 | var _mkdirp = _interopRequireDefault(require("mkdirp")); 9 | 10 | var _prompts = require("./prompts"); 11 | 12 | var _utils = require("./utils"); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 15 | 16 | const getTeamId = async token => { 17 | try { 18 | const { 19 | data: { 20 | teams = [] 21 | } 22 | } = await _axios.default.get('https://vercel.com/api/v2/teams', { 23 | headers: { 24 | Authorization: token 25 | } 26 | }); 27 | return await (0, _prompts.promptForTeam)([{ 28 | name: 'Personal project (NO TEAM)', 29 | id: false 30 | }, ...teams]); 31 | } catch (err) { 32 | console.log(_colors.default.red('Cannot download teams list. Please check your authorization token !')); 33 | process.exit(0); 34 | } 35 | }; 36 | 37 | const getDeployment = async env => { 38 | try { 39 | const { 40 | data: { 41 | deployments = [] 42 | } 43 | } = await _axios.default.get((0, _utils.appendTeamId)(`https://vercel.com/api/v6/deployments`, env.TEAM_ID), { 44 | headers: { 45 | Authorization: env.AUTHORIZATION_TOKEN 46 | } 47 | }); 48 | 49 | if (!deployments.length > 0) { 50 | console.log(_colors.default.red('No deployments found for your choices. Exiting...')); 51 | process.exit(); 52 | } 53 | 54 | const projectName = await (0, _prompts.promptForProjectName)([...new Set(deployments.map(project => project.name))]); 55 | console.log(`Getting list of deployments for ${projectName}`); 56 | return await (0, _prompts.promptForProjectUrl)(deployments.filter(deployment => deployment.name === projectName)); 57 | } catch (err) { 58 | console.log(_colors.default.red('Cannot get deployment UID. Please raise an issue here: https://github.com/CalinaCristian/source-from-vercel-deployment/issues !')); 59 | process.exit(0); 60 | } 61 | }; 62 | 63 | (async () => { 64 | let env = { 65 | DEPLOYMENT_URL: '', 66 | DEPLOYMENT_FILE_URL: '', 67 | AUTHORIZATION_TOKEN: '', 68 | OUTPUT_DIRECTORY: './deployment_source', 69 | TEAM_ID: false 70 | }; 71 | env.AUTHORIZATION_TOKEN = (0, _utils.getAuthToken)(process.env.VERCEL_AUTH_TOKEN ?? (await (0, _prompts.promptForAuthorizationToken)())); 72 | console.log(_colors.default.yellow('Getting list of teams...')); 73 | env.TEAM_ID = await getTeamId(env.AUTHORIZATION_TOKEN); 74 | console.log(_colors.default.yellow('Getting list of deployments...This might take a while...')); 75 | const { 76 | deploymentUid, 77 | deploymentUrl 78 | } = await getDeployment(env); 79 | env.DEPLOYMENT_URL = `https://vercel.com/api/file-tree/${deploymentUrl}?base=out`; 80 | env.DEPLOYMENT_FILE_URL = `https://vercel.com/api/v6/deployments/${deploymentUid}/files/outputs?file=`; 81 | env.OUTPUT_DIRECTORY = (await (0, _prompts.promptForOutputDirectory)()) || env.OUTPUT_DIRECTORY; 82 | console.log(_colors.default.yellow('Starting the process of recreating the structure...')); 83 | const getDeploymentStructureURL = (0, _utils.appendTeamId)(env.DEPLOYMENT_URL, env.TEAM_ID, '&'); 84 | 85 | try { 86 | const { 87 | data 88 | } = await _axios.default.get(getDeploymentStructureURL, { 89 | headers: { 90 | Authorization: env.AUTHORIZATION_TOKEN 91 | } 92 | }); 93 | (0, _mkdirp.default)(env.OUTPUT_DIRECTORY); 94 | (0, _utils.parseStructure)(data, env.OUTPUT_DIRECTORY, env); 95 | } catch (err) { 96 | console.log(err.message); 97 | console.log(_colors.default.red('Cannot recreate the file tree. Please raise an issue here: https://github.com/CalinaCristian/source-from-vercel-deployment/issues !')); 98 | process.exit(0); 99 | } 100 | })(); -------------------------------------------------------------------------------- /dist/prompts.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.promptForTeam = exports.promptForProjectUrl = exports.promptForProjectName = exports.promptForOutputDirectory = exports.promptForAuthorizationToken = void 0; 7 | 8 | var _colors = _interopRequireDefault(require("colors")); 9 | 10 | var _fuzzy = _interopRequireDefault(require("fuzzy")); 11 | 12 | var _inquirer = _interopRequireDefault(require("inquirer")); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 15 | 16 | _inquirer.default.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); 17 | 18 | const promptForAuthorizationToken = async () => { 19 | const { 20 | AUTHORIZATION_TOKEN 21 | } = await _inquirer.default.prompt([{ 22 | type: 'input', 23 | name: 'AUTHORIZATION_TOKEN', 24 | message: _colors.default.magenta('Authorization token (Generate one here https://vercel.com/account/tokens and save it to env variable VERCEL_AUTH_TOKEN to not be asked anymore or paste it here)') 25 | }]); 26 | return AUTHORIZATION_TOKEN; 27 | }; 28 | 29 | exports.promptForAuthorizationToken = promptForAuthorizationToken; 30 | 31 | const promptForTeam = async teams => { 32 | const { 33 | TEAM_ID 34 | } = await _inquirer.default.prompt([{ 35 | type: 'autocomplete', 36 | name: 'TEAM_ID', 37 | message: 'Choose the team that your project is in (or Personal Project if it\'s not in a team)', 38 | source: (_, input) => { 39 | return new Promise(resolve => { 40 | const fuzzyResult = _fuzzy.default.filter(input || '', teams.map(project => project.name)); 41 | 42 | resolve(fuzzyResult.map(result => ({ 43 | name: _colors.default.cyan(result.original), 44 | value: teams[teams.findIndex(project => project.name === result.original)].id 45 | }))); 46 | }); 47 | }, 48 | pageSize: 10 49 | }]); 50 | return TEAM_ID; 51 | }; 52 | 53 | exports.promptForTeam = promptForTeam; 54 | 55 | const promptForProjectName = async projectNames => { 56 | const { 57 | DEPLOYMENT_NAME 58 | } = await _inquirer.default.prompt([{ 59 | type: 'autocomplete', 60 | name: 'DEPLOYMENT_NAME', 61 | message: 'Choose the project name that you wish to download from', 62 | source: (_, input) => { 63 | return new Promise(resolve => { 64 | const fuzzyResult = _fuzzy.default.filter(input || '', projectNames); 65 | 66 | resolve(fuzzyResult.map(result => ({ 67 | name: _colors.default.cyan(result.original), 68 | value: result.original 69 | }))); 70 | }); 71 | }, 72 | pageSize: 10 73 | }]); 74 | return DEPLOYMENT_NAME; 75 | }; 76 | 77 | exports.promptForProjectName = promptForProjectName; 78 | 79 | const promptForProjectUrl = async projects => { 80 | const { 81 | PROJECT_URL 82 | } = await _inquirer.default.prompt([{ 83 | type: 'autocomplete', 84 | name: 'PROJECT_URL', 85 | message: 'Choose the deployment that you wish to download from (first is the latest)', 86 | source: (_, input) => { 87 | return new Promise(resolve => { 88 | const fuzzyResult = _fuzzy.default.filter(input || '', projects.map(project => project.url)); 89 | 90 | resolve(fuzzyResult.map(result => ({ 91 | name: _colors.default.cyan(result.original), 92 | value: { 93 | deploymentUid: projects[projects.findIndex(project => project.url === result.original)].uid, 94 | deploymentUrl: projects[projects.findIndex(project => project.url === result.original)].url 95 | } 96 | }))); 97 | }); 98 | }, 99 | pageSize: 10 100 | }]); 101 | return PROJECT_URL; 102 | }; 103 | 104 | exports.promptForProjectUrl = promptForProjectUrl; 105 | 106 | const promptForOutputDirectory = async () => { 107 | const { 108 | OUTPUT_DIRECTORY 109 | } = await _inquirer.default.prompt([{ 110 | type: 'input', 111 | name: 'OUTPUT_DIRECTORY', 112 | message: _colors.default.magenta('Where do you want the source to be placed? (Default: ./deployment_source)') 113 | }]); 114 | return OUTPUT_DIRECTORY; 115 | }; 116 | 117 | exports.promptForOutputDirectory = promptForOutputDirectory; -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.parseStructure = exports.getAuthToken = exports.appendTeamId = void 0; 7 | 8 | var _axios = _interopRequireDefault(require("axios")); 9 | 10 | var _colors = _interopRequireDefault(require("colors")); 11 | 12 | var _fsExtra = _interopRequireDefault(require("fs-extra")); 13 | 14 | var _path = require("path"); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | const getAuthToken = token => token.includes('Bearer') || token.includes('bearer') ? token[0].toUpperCase() + token.slice(1) : `bearer ${token}`; 19 | 20 | exports.getAuthToken = getAuthToken; 21 | 22 | const appendTeamId = (url, teamId, symbol = '?') => teamId ? `${url}${symbol}teamId=${teamId}` : url; 23 | 24 | exports.appendTeamId = appendTeamId; 25 | 26 | const generateFile = async (fileName, currentPath, env) => { 27 | const url = appendTeamId(`${env.DEPLOYMENT_FILE_URL}${fileName}`, env.TEAM_ID, '&'); 28 | 29 | try { 30 | const savePath = (0, _path.join)(currentPath, fileName); 31 | console.log(_colors.default.yellow('Downloading file: '), _colors.default.cyan(fileName), _colors.default.yellow(' to path: '), _colors.default.cyan(savePath)); 32 | const { 33 | data 34 | } = await _axios.default.get(url, { 35 | responseType: 'stream', 36 | headers: { 37 | Authorization: env.AUTHORIZATION_TOKEN 38 | } 39 | }); 40 | data.pipe(_fsExtra.default.createWriteStream(savePath)); 41 | } catch (err) { 42 | console.log(_colors.default.red(`Cannot download from ${url}. Please raise an issue here: https://github.com/CalinaCristian/source-from-vercel-deployment/issues !`)); 43 | process.exit(); 44 | } 45 | }; 46 | 47 | const generateDirectory = path => { 48 | try { 49 | _fsExtra.default.mkdirpSync(path); 50 | } catch (err) { 51 | console.log(_colors.default.red(`Cannot write directory on path: ${path} !`)); 52 | process.exit(); 53 | } 54 | }; 55 | 56 | const parseCurrent = (node, currentPath, env) => { 57 | if (node.type === 'directory') { 58 | generateDirectory((0, _path.join)(currentPath, node.name)); 59 | parseStructure(node.children, (0, _path.join)(currentPath, node.name), env); 60 | } else if (node.type === 'file') { 61 | generateFile(node.name, currentPath, env); 62 | } 63 | }; 64 | 65 | const parseStructure = (folderStructure, currentPath, env) => { 66 | if (folderStructure) { 67 | folderStructure.forEach(structure => parseCurrent(structure, currentPath, env)); 68 | } 69 | }; 70 | 71 | exports.parseStructure = parseStructure; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "source-from-vercel-deployment", 3 | "version": "4.1.0", 4 | "description": "A simple package made for downloading the source code from your Vercel Deployment.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "npm run build && node dist/index.js", 8 | "npm-version": "npm version", 9 | "bump:pre": "npm run bump -- premajor --preid=r.c -m \"chore: bump version\"", 10 | "bump": "npm run npm-version -m \"chore: bump version\"", 11 | "bump:patch": "npm run bump -- patch", 12 | "bump:minor": "npm run bump -- minor", 13 | "bump:major": "npm run bump -- major", 14 | "update-changelog": "npx auto-changelog --commit-limit false --sort-commits date-desc --template ./changelog.hbs", 15 | "build": "babel src --out-dir dist", 16 | "version": "npm run build", 17 | "postversion": "npm run update-changelog && git add CHANGELOG.md && git commit --amend --no-edit && git push --tags" 18 | }, 19 | "auto-changelog": { 20 | "replaceText": { 21 | "^(.*)(? { 21 | try { 22 | const { data: { teams = [] } } = await axios.get('https://vercel.com/api/v2/teams', { 23 | headers: { 24 | Authorization: token 25 | } 26 | }); 27 | return await promptForTeam([{name: 'Personal project (NO TEAM)', id: false}, ...teams]); 28 | } catch (err) { 29 | console.log(colors.red('Cannot download teams list. Please check your authorization token !')); 30 | process.exit(0); 31 | } 32 | }; 33 | 34 | const getDeployment = async (env) => { 35 | try { 36 | const { data: { deployments = [] } } = await axios.get(appendTeamId(`https://vercel.com/api/v6/deployments`, env.TEAM_ID), { 37 | headers: { 38 | Authorization: env.AUTHORIZATION_TOKEN 39 | } 40 | }); 41 | 42 | if (!deployments.length > 0) { 43 | console.log(colors.red('No deployments found for your choices. Exiting...')); 44 | process.exit(); 45 | } 46 | 47 | const projectName = await promptForProjectName([...new Set(deployments.map(project => project.name))]); 48 | console.log(`Getting list of deployments for ${projectName}`); 49 | 50 | return await promptForProjectUrl(deployments.filter(deployment => deployment.name === projectName)); 51 | } catch (err) { 52 | console.log(colors.red('Cannot get deployment UID. Please raise an issue here: https://github.com/CalinaCristian/source-from-vercel-deployment/issues !')); 53 | process.exit(0); 54 | } 55 | }; 56 | 57 | (async () => { 58 | let env = { 59 | DEPLOYMENT_URL: '', 60 | DEPLOYMENT_FILE_URL: '', 61 | AUTHORIZATION_TOKEN: '', 62 | OUTPUT_DIRECTORY: './deployment_source', 63 | TEAM_ID: false 64 | }; 65 | env.AUTHORIZATION_TOKEN = getAuthToken(process.env.VERCEL_AUTH_TOKEN ?? (await promptForAuthorizationToken())); 66 | 67 | console.log(colors.yellow('Getting list of teams...')); 68 | env.TEAM_ID = await getTeamId(env.AUTHORIZATION_TOKEN); 69 | 70 | console.log(colors.yellow('Getting list of deployments...This might take a while...')); 71 | const { deploymentUid, deploymentUrl } = await getDeployment(env); 72 | 73 | env.DEPLOYMENT_URL = `https://vercel.com/api/file-tree/${deploymentUrl}?base=out`; 74 | env.DEPLOYMENT_FILE_URL = `https://vercel.com/api/v6/deployments/${deploymentUid}/files/outputs?file=`; 75 | 76 | env.OUTPUT_DIRECTORY = await promptForOutputDirectory() || env.OUTPUT_DIRECTORY; 77 | 78 | console.log(colors.yellow('Starting the process of recreating the structure...')); 79 | const getDeploymentStructureURL = appendTeamId(env.DEPLOYMENT_URL, env.TEAM_ID, '&'); 80 | 81 | try { 82 | const { data } = await axios.get(getDeploymentStructureURL, { 83 | headers: { 84 | Authorization: env.AUTHORIZATION_TOKEN 85 | } 86 | }); 87 | 88 | mkdirp(env.OUTPUT_DIRECTORY); 89 | parseStructure(data, env.OUTPUT_DIRECTORY, env); 90 | } catch (err) { 91 | console.log(err.message); 92 | console.log(colors.red('Cannot recreate the file tree. Please raise an issue here: https://github.com/CalinaCristian/source-from-vercel-deployment/issues !')); 93 | process.exit(0); 94 | } 95 | })(); -------------------------------------------------------------------------------- /src/prompts.js: -------------------------------------------------------------------------------- 1 | import colors from 'colors'; 2 | import fuzzy from 'fuzzy'; 3 | import inquirer from 'inquirer'; 4 | 5 | inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); 6 | 7 | export const promptForAuthorizationToken = async() => { 8 | const { AUTHORIZATION_TOKEN } = await inquirer.prompt([ 9 | { 10 | type: 'input', 11 | name: 'AUTHORIZATION_TOKEN', 12 | message: colors.magenta('Authorization token (Generate one here https://vercel.com/account/tokens and save it to env variable VERCEL_AUTH_TOKEN to not be asked anymore or paste it here)'), 13 | } 14 | ]); 15 | 16 | return AUTHORIZATION_TOKEN; 17 | }; 18 | 19 | export const promptForTeam = async(teams) => { 20 | const { TEAM_ID } = await inquirer.prompt([{ 21 | type: 'autocomplete', 22 | name: 'TEAM_ID', 23 | message: 'Choose the team that your project is in (or Personal Project if it\'s not in a team)', 24 | source: (_, input) => { 25 | return new Promise((resolve) => { 26 | const fuzzyResult = fuzzy.filter(input || '', teams.map(project => project.name)); 27 | resolve( 28 | fuzzyResult.map(result => ({ 29 | name: colors.cyan(result.original), 30 | value: teams[teams.findIndex(project => project.name === result.original)].id 31 | })) 32 | ); 33 | }); 34 | }, 35 | pageSize: 10 36 | }]); 37 | 38 | return TEAM_ID; 39 | }; 40 | 41 | export const promptForProjectName = async(projectNames) => { 42 | const { DEPLOYMENT_NAME } = await inquirer.prompt([{ 43 | type: 'autocomplete', 44 | name: 'DEPLOYMENT_NAME', 45 | message: 'Choose the project name that you wish to download from', 46 | source: (_, input) => { 47 | return new Promise((resolve) => { 48 | const fuzzyResult = fuzzy.filter(input || '', projectNames); 49 | resolve( 50 | fuzzyResult.map(result => ({ 51 | name: colors.cyan(result.original), 52 | value: result.original 53 | })) 54 | ); 55 | }); 56 | }, 57 | pageSize: 10 58 | }]); 59 | 60 | return DEPLOYMENT_NAME; 61 | }; 62 | 63 | export const promptForProjectUrl = async(projects) => { 64 | const { PROJECT_URL } = await inquirer.prompt([{ 65 | type: 'autocomplete', 66 | name: 'PROJECT_URL', 67 | message: 'Choose the deployment that you wish to download from (first is the latest)', 68 | source: (_, input) => { 69 | return new Promise((resolve) => { 70 | const fuzzyResult = fuzzy.filter(input || '', projects.map(project => project.url)); 71 | resolve( 72 | fuzzyResult.map(result => ({ 73 | name: colors.cyan(result.original), 74 | value: { 75 | deploymentUid: projects[projects.findIndex(project => project.url === result.original)].uid, 76 | deploymentUrl: projects[projects.findIndex(project => project.url === result.original)].url, 77 | } 78 | })) 79 | ); 80 | }); 81 | }, 82 | pageSize: 10 83 | }]); 84 | 85 | return PROJECT_URL; 86 | }; 87 | 88 | export const promptForOutputDirectory = async() => { 89 | const { OUTPUT_DIRECTORY } = await inquirer.prompt([ 90 | { 91 | type: 'input', 92 | name: 'OUTPUT_DIRECTORY', 93 | message: colors.magenta('Where do you want the source to be placed? (Default: ./deployment_source)') 94 | } 95 | ]); 96 | 97 | return OUTPUT_DIRECTORY; 98 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import colors from 'colors'; 3 | import fsExtra from 'fs-extra'; 4 | import { join } from 'path'; 5 | 6 | export const getAuthToken = token => token.includes('Bearer') || token.includes('bearer') 7 | ? token[0].toUpperCase() + token.slice(1) 8 | : `bearer ${token}`; 9 | 10 | export const appendTeamId = (url, teamId, symbol = '?') => teamId ? `${url}${symbol}teamId=${teamId}` : url; 11 | 12 | const generateFile = async (fileName, currentPath, env) => { 13 | const url = appendTeamId(`${env.DEPLOYMENT_FILE_URL}${fileName}`, env.TEAM_ID, '&'); 14 | 15 | try { 16 | const savePath = join(currentPath, fileName); 17 | console.log(colors.yellow('Downloading file: '), colors.cyan(fileName), colors.yellow(' to path: '), colors.cyan(savePath)); 18 | const { data } = await axios.get(url, { 19 | responseType:'stream', 20 | headers: { 21 | Authorization: env.AUTHORIZATION_TOKEN 22 | } 23 | }); 24 | data.pipe(fsExtra.createWriteStream(savePath)); 25 | } catch (err) { 26 | console.log(colors.red(`Cannot download from ${url}. Please raise an issue here: https://github.com/CalinaCristian/source-from-vercel-deployment/issues !`)); 27 | process.exit(); 28 | } 29 | }; 30 | 31 | const generateDirectory = (path) => { 32 | try { 33 | fsExtra.mkdirpSync(path); 34 | } catch (err) { 35 | console.log(colors.red(`Cannot write directory on path: ${path} !`)); 36 | process.exit(); 37 | } 38 | }; 39 | 40 | const parseCurrent = (node, currentPath, env) => { 41 | if (node.type === 'directory') { 42 | generateDirectory(join(currentPath, node.name)); 43 | parseStructure(node.children, join(currentPath, node.name), env); 44 | } else if (node.type === 'file') { 45 | generateFile(node.name, currentPath, env); 46 | } 47 | }; 48 | 49 | export const parseStructure = (folderStructure, currentPath, env) => { 50 | if (folderStructure) { 51 | folderStructure.forEach(structure => parseCurrent(structure, currentPath, env)); 52 | } 53 | }; 54 | --------------------------------------------------------------------------------