├── .gitignore ├── .jshintrc ├── CHANGELOG.md ├── README.md ├── bin └── divshot.js ├── index.js ├── lib ├── commands │ ├── account.js │ ├── apps.js │ ├── auth.js │ ├── cert.js │ ├── changelog.js │ ├── config.js │ ├── create.js │ ├── destroy.js │ ├── domains.js │ ├── dump.js │ ├── emails.js │ ├── env.js │ ├── files.js │ ├── hooks.js │ ├── index.js │ ├── init.js │ ├── login.js │ ├── logout.js │ ├── open.js │ ├── performance.js │ ├── promote.js │ ├── protect.js │ ├── pull.js │ ├── purge.js │ ├── push.js │ ├── rename.js │ ├── request.js │ ├── rollback.js │ ├── server.js │ ├── stats.js │ ├── status.js │ └── unprotect.js ├── cwd.js ├── environments.js ├── errors.js ├── helpers │ ├── bundle_files.js │ ├── check_files_sizes.js │ ├── command.js │ ├── format_user_emails.js │ ├── loading.js │ └── open_with_fallback.js ├── logo.txt ├── templates │ ├── error.html │ └── index.html ├── user.js └── validators │ ├── slug.js │ └── yes_no.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | lib/config/user.json -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "expr": true, 3 | "curly": false, 4 | "freeze": true, 5 | "noarg": true, 6 | "nonew": true, 7 | "plusplus": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "latedef": false, 11 | "newcap": true, 12 | "noarg": true, 13 | "sub": true, 14 | "undef": true, 15 | "unused": false, 16 | "boss": true, 17 | "eqnull": true, 18 | "node": true, 19 | "quotmark": "single", 20 | "globals": { 21 | "require": true, 22 | "module": true, 23 | "exports": true, 24 | "it": true, 25 | "describe": true, 26 | "beforeEach": true, 27 | "afterEach": true, 28 | "chai": true, 29 | "Regex": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.10.1 4 | 5 | **Released on 2-24-2015** 6 | 7 | * use Superstatic 1.2.0. This mainly adds the ability to define your custom routes in a specific order 8 | 9 | ## 1.10.0 10 | 11 | **Released on 2-24-2015** 12 | 13 | * **[#160](https://github.com/divshot/divshot-cli/issues/160)** - ensure app config gets posted as json on push 14 | 15 | ## 1.9.1 16 | 17 | **Released on 2-24-2015** 18 | 19 | * adds description in usage screen for the purge command 20 | 21 | ## [1.9.0](https://github.com/divshot/divshot-cli/issues?q=is%3Aissue+is%3Aclosed+milestone%3A1.9) 22 | 23 | **Released on 2-23-2015** 24 | 25 | * **[157](https://github.com/divshot/divshot-cli/issues/157)** - fixed `-v` and `--version` flags which weren't working 26 | * **[158](https://github.com/divshot/divshot-cli/issues/158)** - added cache purge command 27 | 28 | ## [1.8.0](https://github.com/divshot/divshot-cli/issues?q=milestone%3A1.8) 29 | 30 | **Released on 2-12-2015** 31 | 32 | * **[#127](https://github.com/divshot/divshot-cli/issues/127)** - Divshot protect should display more informative error when environment is empty 33 | * **[#128](https://github.com/divshot/divshot-cli/issues/128)** - Divshot protect error should display usage info 34 | * **[#152](https://github.com/divshot/divshot-cli/issues/152)** - Custom domains on any environment 35 | * **[#153](https://github.com/divshot/divshot-cli/issues/153)** - Better error message environments that don't exist on promote 36 | 37 | ## [1.7.0](https://github.com/divshot/divshot-cli/issues?q=milestone%3A1.7+is%3Aclosed) 38 | 39 | **Released on 2-4-2015** 40 | 41 | * **[#142](https://github.com/divshot/divshot-cli/issues/142)** - Remove all occurences of Divshot.io 42 | * **[#143](https://github.com/divshot/divshot-cli/issues/143)** - Raw http requests 43 | * **[#145](https://github.com/divshot/divshot-cli/issues/145)** - Rollback release to any version 44 | * **[#146](https://github.com/divshot/divshot-cli/issues/146)** - Handle SSL uploading errors 45 | * **[#147](https://github.com/divshot/divshot-cli/issues/147)** - Ability to remove environment variables 46 | * **[#148](https://github.com/divshot/divshot-cli/issues/148)** - Update divshot-push to 1.3 47 | 48 | * * * 49 | 50 | (For releases previous to **1.7.0**, see [releases](https://github.com/divshot/divshot-cli/releases)) 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Divshot CLI [![NPM version](http://img.shields.io/npm/v/divshot-cli.svg?style=flat-square)](http://npmjs.org/package/divshot-cli) ![NPM download count](https://img.shields.io/npm/dm/divshot-cli.svg?style=flat-square) 2 | 3 | CLI for Divshot 4 | 5 | #### [Commands](#commands-1) 6 | 7 | * [account](#account) - display basic account details 8 | * [account:redeem](#accountredeem) - redeem a voucher and credit it to your account 9 | * [apps](#apps) - list your apps 10 | * [auth:token](#authtoken) - print out your access token 11 | * [cert](#cert) - upload an SSL certificate 12 | * [changelog](#changelog) - view the cli changelog 13 | * [config](#config) - list, set, or remove keys and values from your app 14 | * [config:add](#configadd) - add a value to the config file 15 | * [config:remove](#configremove) - remove a value from the config file 16 | * [create](#create) - create a new app 17 | * [destroy](#destroy) - delete an app 18 | * [domains](#domains) - list your domains 19 | * [domains:add](#domainsadd) - add a custom domain to your app 20 | * [domains:remove](#domainsremove) - remove a custom domain from your app 21 | * [emails](#emails) - list emails associated with your app 22 | * [emails:add](#emailsadd) - add an email to your app 23 | * [emails:remove](#emailsremove) - remove an email from your app 24 | * [emails:resend](#emailsresend) - resend the confirmation email 25 | * [env](#env) - list environment variables for your app 26 | * [env:add](#envadd) - add environment variables to your app 27 | * [env:remove](#envremove) - remove environment variables from your app 28 | * [env:pull](#envpull) - copy environment data to your local environment 29 | * [files](#files) - list the current files associated with the given environment 30 | * [help](#help) - get help with common commands 31 | * [hooks](#hooks) - list webhooks 32 | * [hooks:add](#hooksadd) - add a webhook 33 | * [hooks:remove](#hooksremove) - remove a webhook 34 | * [hooks:pause](#hookspause) - pause a webhook 35 | * [hooks:resume](#hooksresume) - resume a webhook 36 | * [init](#init) - step by step guide to initiate an app in the current directory 37 | * [login](#login) - login to Divshot 38 | * [logout](#logout) - logout from Divshot 39 | * [open](#open) - open the current app in your default browser 40 | * [performance](#performance) - show if current app has performance status 41 | * [performance:on](#performanceon) - give the current app performance status 42 | * [performance:off](#performanceoff) - remove performance status from the current app 43 | * [promote](#promote) - promote one environment to another 44 | * [protect](#protect) - add http basic auth to any environment 45 | * [pull](#pull) - download the current files for a given environment into a directory 46 | * [purge](#purge) - purge the cache for your app 47 | * [push](#push) - deploy your app to the specified environment 48 | * [rename](#rename) - change the name of an app 49 | * [rollback](#rollback) - rollback an environment to a previous release 50 | * [server](#server) - start server for local dev 51 | * [stats](#stats) - show stats info for your app 52 | * [status](#status) - show release info for each environment 53 | * [unprotect](#unprotect) - remove basic auth from an environment on your app 54 | 55 | #### Command Options 56 | 57 | * `-h, --help` - show the help menu 58 | * `-v, --version` - show current version of Divshot CLI 59 | * `-t, --token [token]` - manually pass access token 60 | * `-a, --app [app name]` - manually supply an app name 61 | * `-c, --config [config]` - use a different config file 62 | * `-o, --org [org name]` - set command to operate on the given organization 63 | * `--timeout [timeout]` - set the command timeout in milliseconds 64 | 65 | #### Environments 66 | 67 | * ` development ` - this is the default environment during app deployment 68 | * ` staging ` 69 | * ` production ` 70 | 71 | Each environment is immediately available and deployed at the following URL scheme: **http://[environment].[app name].divshot.io**. You may reference [Divshot Builds and Environments](http://docs.divshot.io/guides/builds) for a more detailed explanation. 72 | 73 | ## Install 74 | 75 | ``` 76 | npm install divshot-cli -g 77 | ``` 78 | 79 | ## Commands 80 | 81 | ### account 82 | 83 | ``` 84 | divshot account 85 | ``` 86 | 87 | Display your basic account details. 88 | 89 | ### account:redeem 90 | 91 | ``` 92 | divshot account:redeem [voucher code] 93 | ``` 94 | 95 | Redeem a voucher and credit it to your account. 96 | 97 | ### apps 98 | 99 | ``` 100 | divshot apps 101 | ``` 102 | 103 | List your Divshot apps. 104 | 105 | ### auth:token 106 | 107 | ``` 108 | divshot auth:token 109 | ``` 110 | 111 | Print out your access token. This token is used to authenticate you with the Divshot API. 112 | 113 | ### config 114 | 115 | ``` 116 | divshot config 117 | ``` 118 | 119 | List the keys and values from your Divshot app config file. See [Divshot configuration reference](http://docs.divshot.io/guides/configuration) for more details on these values. 120 | 121 | ### cert 122 | 123 | ``` 124 | divshot cert [path/to/certificate.crt] [path/to/key.key] 125 | ``` 126 | 127 | Upload your application's SSL certificate. 128 | 129 | ### changelog 130 | 131 | ``` 132 | divshot changelog 133 | ``` 134 | 135 | View the Divshot CLI changelog 136 | 137 | ### config:add 138 | 139 | ``` 140 | divshot config:add [key] [value] 141 | ``` 142 | 143 | Add a value to your Divshot app config file. See [Divshot configuration reference](http://docs.divshot.io/guides/configuration) for more details on these values. 144 | 145 | ### config:remove 146 | 147 | ``` 148 | divshot config:remove [key] 149 | ``` 150 | 151 | Remove a value from your Divshot app config file. See [Divshot configuration reference](http://docs.divshot.io/guides/configuration) for more details on these values. 152 | 153 | ### create 154 | 155 | ``` 156 | divshot create [app name] 157 | ``` 158 | 159 | Create a new Divshot app. If no app name is provided, it attempts to read from your Divshot configuration file. It that does not exist, it will prompt you for an app name. You can easily create a new Divshot app locally and remotely by using [` divshot init `](#init). 160 | 161 | ### destroy 162 | 163 | 164 | ``` 165 | divshot destroy [app name] 166 | ``` 167 | 168 | Delete a Divshot app. This is permanent and immediate. It removes not only your files, but it disables the subdomain associated with the application. 169 | 170 | ### domains 171 | 172 | ``` 173 | divshot domains 174 | ``` 175 | 176 | See a list of all custom domains associated with your app. For more in-depth usage, see [Divshot Custom Domains](http://docs.divshot.io/guides/domains). 177 | 178 | ### domains:add 179 | 180 | ``` 181 | divshot domains:add [environment] [domain] 182 | ``` 183 | 184 | Add a custom domain to your app. You may see a list of your domains with [` divshot domains `](#domains). For more in-depth usage, see [Divshot Custom Domains](http://docs.divshot.io/guides/domains). 185 | 186 | `environment` is optional and defaults to `production`. 187 | 188 | ### domains:remove 189 | 190 | ``` 191 | divshot domains:remove [domain] 192 | ``` 193 | 194 | Remove a custom domain from your app. You may see a list of your domains with [` divshot domains `](#domains). For more in-depth usage, see [Divshot Custom Domains](http://docs.divshot.io/guides/domains). 195 | 196 | ### emails 197 | 198 | ``` 199 | divshot emails 200 | ``` 201 | 202 | Show any emails associated with this app. Also shows which emails have been approved and which emails are pending authorization by the email owner. 203 | 204 | ### emails:add 205 | 206 | ``` 207 | divshot emails:add [email] 208 | ``` 209 | 210 | Add an email to the current app. Once added, the email will receive an email that the email owner must use to authorize the email address. 211 | 212 | ### emails:remove 213 | 214 | ``` 215 | divshot emails:remove [email] 216 | ``` 217 | 218 | Remove an email from the current app. 219 | 220 | ### emails:resend 221 | 222 | ``` 223 | divshot emails:resend [email] 224 | ``` 225 | 226 | Resend the the authorization email for the given email address. 227 | 228 | ### env 229 | 230 | ``` 231 | divshot env [environment] 232 | ``` 233 | 234 | List the key/value pairs of environment variables associated with the given environment on the current app. See [Environment Variables](http://docs.divshot.com/guides/environment-variables) for more details. 235 | 236 | ### env:add 237 | 238 | ``` 239 | divshot env:add [environment] KEY=value KEY2=value ... 240 | ``` 241 | 242 | Add environment variables to the current app. See [Environment Variables](http://docs.divshot.com/guides/environment-variables) for more details. 243 | 244 | ### env:remove 245 | 246 | ``` 247 | divshot env:remove [environment] KEY1 KEY2 ... 248 | ``` 249 | 250 | Remove environment variables from the current app. See [Environment Variables](http://docs.divshot.com/guides/environment-variables) for more details. 251 | 252 | ### env:pull 253 | 254 | ``` 255 | divshot env:pull [environment] 256 | ``` 257 | 258 | Copy environment data to your local environment. Creates a `.env.json` file that is available to your app with `__env.js` or `__env.json`. See [Environment Variables](http://docs.divshot.com/guides/environment-variables) for more details. 259 | 260 | ### files 261 | 262 | ``` 263 | divshot files [environment] 264 | ``` 265 | 266 | List the files currently associated with the given environment. These are the files that were deployed to Divshot using [divshot push](#push) 267 | 268 | ### help 269 | 270 | ``` 271 | divshot help 272 | ``` 273 | 274 | Get help with common Divshot commands. Lists all the available commands. 275 | 276 | If you need help with a specific command, you may specify that command after the word *help*. 277 | 278 | ``` 279 | divshot help [command] 280 | ``` 281 | 282 | #### hooks 283 | 284 | ``` 285 | divshot hooks 286 | ``` 287 | 288 | Get a list of webhooks for the current app. For more documentation on using Divshot webhooks, see the [Webhook Documentation](http://docs.divshot.com/guides/webhooks). 289 | 290 | #### hooks:add 291 | 292 | ``` 293 | divshot hooks:add [url] 294 | ``` 295 | 296 | Add a webhook to the current app. For more documentation on using Divshot webhooks, see the [Webhook Documentation](http://docs.divshot.com/guides/webhooks). 297 | 298 | #### hooks:remove 299 | 300 | ``` 301 | divshot hooks:remove [optional url] 302 | ``` 303 | 304 | Remove a webhook from the current app. If no url is provided, you will be prompted with a list of your webhooks to select for removal. For more documentation on using Divshot webhooks, see the [Webhook Documentation](http://docs.divshot.com/guides/webhooks). 305 | 306 | #### hooks:pause 307 | 308 | ``` 309 | divshot hooks:pause [optional url] 310 | ``` 311 | 312 | Pause a webhook without deleting it for the current app. If no url is provided, you will be prompted with a list of your webhooks to select to pause. For more documentation on using Divshot webhooks, see the [Webhook Documentation](http://docs.divshot.com/guides/webhooks). 313 | 314 | #### hooks:resume 315 | 316 | ``` 317 | divshot hooks:resume [optional url] 318 | ``` 319 | 320 | Resume a webhook for the current app. If no url is provided, you will be prompted with a list of your webhooks to select to resume. For more documentation on using Divshot webhooks, see the [Webhook Documentation](http://docs.divshot.com/guides/webhooks). 321 | 322 | ### init 323 | 324 | ``` 325 | divshot init 326 | ``` 327 | 328 | Step by step guide to initiate an app in the current directory. The steps you are taken through are as follows: 329 | 330 | 1. ` name ` - app name 331 | 2. ` root ` - the root directory of the app relative to the current directory 332 | 3. ` error page ` - the relative path or absolute url of an error/not found page to display in your app 333 | 4. ` create app ` - do you want to create a new app on Divshot upon completing these steps? (As opposed to only creating the app locally) 334 | 335 | Once you initiated your app, the *root* directory will now contain a ` divshot.json ` file with your settings. You may reference [Divshot configuration reference](http://docs.divshot.io/guides/configuration) for a more detailed description of this file. 336 | 337 | ### login 338 | 339 | ``` 340 | divshot login 341 | ``` 342 | 343 | Login to your Divshot account. 344 | 345 | ### logout 346 | 347 | ``` 348 | divshot logout 349 | ``` 350 | 351 | Logout of your account. 352 | 353 | ### open 354 | 355 | ``` 356 | divshot open [optional environment] 357 | ``` 358 | 359 | Open app in your default browser. 360 | 361 | Example: 362 | 363 | * ` divshot open ` - Opens the production, CDN environment of your app 364 | * ` divshot open development ` - Opens up the development environment of your app 365 | 366 | ### performance 367 | 368 | ``` 369 | divshot performance 370 | ``` 371 | 372 | Show if current app has performance status. For more details on apps and performance status see [High Performance Mode Documenation](http://docs.divshot.io/guides/high-performance) 373 | 374 | 375 | ### performance:on 376 | 377 | ``` 378 | divshot performance:on 379 | ``` 380 | 381 | Give the current app performance status. For more details on apps and performance status see [High Performance Mode Documenation](http://docs.divshot.io/guides/high-performance) 382 | 383 | ### performance:off 384 | 385 | ``` 386 | divshot performance:off 387 | ``` 388 | 389 | Remove performance status from the current app. For more details on apps and performance status see [High Performance Mode Documenation](http://docs.divshot.io/guides/high-performance) 390 | 391 | 392 | ### promote 393 | 394 | ``` 395 | divshot promote [from env] [to env] 396 | ``` 397 | 398 | Promote one environment to another. A typical use case for this command would be to deploy your staging app to production without having to redeploy all the files. See [environments](#environments) for a list of available environments. 399 | 400 | Example promotions 401 | 402 | * ` divshot promote development staging ` - development -> staging 403 | * ` divshot promote staging production ` - staging -> production 404 | 405 | ### protect 406 | 407 | ``` 408 | divshot protect [environment] [username:password] 409 | ``` 410 | 411 | Protect your development and staging environments with [http authentication](http://en.wikipedia.org/wiki/Basic_access_authentication). 412 | 413 | ### pull 414 | 415 | ``` 416 | divshot pull [environment] [optional directory] 417 | ``` 418 | 419 | Download all the files currently associated with the given environment. If no directory is provided, it defaults to the current directory. These files are the files deployed to Divshot using [divshot push](#push) 420 | 421 | ### purge 422 | 423 | ``` 424 | divshot purge [optional environment] 425 | ``` 426 | 427 | Purge the cache for your app. If no environment is provided, all caches for all your environments will be purged. 428 | 429 | ### push 430 | 431 | ``` 432 | divshot push [environment] 433 | ``` 434 | 435 | Deploy your app to the specified environment. If no environment is given, we assume that you mean *production*. The entire push process takes as long as the number of files in your project. Once deployed, your app is immediately available. See [environments](#environments) for a list of available environments. 436 | 437 | ### rename 438 | 439 | ``` 440 | divshot rename [new app name] 441 | ``` 442 | 443 | Rename your app. This changes the subdomain on Divshot and updates your configuration file. It is permanent once complete. 444 | 445 | ### rollback 446 | 447 | ``` 448 | divshot rollback [environment] [version] 449 | ``` 450 | 451 | Rollback the given environment to a previous release. This is useful when buggy code has been deployed. Divshot automatically detects and rolls back to your previous release. See [environments](#environments) for a list of available environments. 452 | 453 | Optionally rollback to a specific version of your app. 454 | 455 | ### server 456 | 457 | ``` 458 | divshot server 459 | ``` 460 | 461 | Start a server for local development. This local server mimics the capabilities of static sites running on [Divshot](http://divshot.io). Refer to the [Divshot documentation](http://docs.divshot.io/guides/configuration) for configuration instructions. 462 | 463 | Server command options: 464 | 465 | * ` -p, --port [port]` - specify the port for the server to run. Defaults to *3474* 466 | * ` -h, --host [hostname]` - specify a custom hostname for your app to run at. Defaults to *localhost* 467 | 468 | ### stats 469 | 470 | ``` 471 | divshot stats 472 | ``` 473 | 474 | Show stats infor for your app 475 | 476 | ### status 477 | 478 | ``` 479 | divshot status [environment] 480 | ``` 481 | 482 | Show release info for each environment. If no environment is specified, the latest release info will be listed for each environment. If an environment is specified, it will list the last few releases for that environment. See [environments](#environments) for a list of available environments. 483 | 484 | ### unprotect 485 | 486 | ``` 487 | divshot unprotect [environment] 488 | ``` 489 | 490 | Unprotect your development and staging environments and remove the authentication. 491 | 492 | 493 | 494 | -------------------------------------------------------------------------------- /bin/divshot.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var cli = require('../index.js'); 4 | var updateNotifier = require('update-notifier'); 5 | var pkg = require('../package.json'); 6 | var updateCheckInterval = 1000 * 60 * 60 * 24 * 7; // 1 week 7 | 8 | // Check for update 9 | var notifier = updateNotifier({ 10 | packageName: pkg.name, 11 | packageVersion: pkg.version, 12 | updateCheckInterval: updateCheckInterval 13 | }); 14 | 15 | if (notifier.update) notifier.notify(); 16 | 17 | cli.run(process.argv); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var Nash = require('nash'); 4 | var homeDir = require('home-dir'); 5 | var Divshot = require('divshot-api'); 6 | var format = require('chalk'); 7 | var format = require('chalk'); 8 | var dumper = require('divshot-dumper'); 9 | 10 | var User = require('./lib/user'); 11 | var Cwd = require('./lib/cwd'); 12 | var environments = require('./lib/environments'); 13 | var commands = require('./lib/commands'); 14 | var errors = require('./lib/errors'); 15 | 16 | var CLIENT_ID = '526753cf2f55bd0002000006'; 17 | var API_HOST = process.env.API_HOST || 'https://api.divshot.com'; 18 | process.env.DIVSHOT_HASHED_BUCKET || (process.env.DIVSHOT_HASHED_BUCKET = "divshot-io-hashed-production"); 19 | 20 | var cliConfigDirectory = path.join(homeDir(), '.divshot'); 21 | var user = new User(cliConfigDirectory); 22 | var cwd = new Cwd(); 23 | var title = fs.readFileSync(__dirname + '/lib/logo.txt') 24 | var description = [ 25 | 'Application-Grade Static Web Hosting\n ', 26 | ' Host single-page apps and static sites with', 27 | ' all the power of a modern application platform.' 28 | ].join('\n'); 29 | 30 | // Set up CLI 31 | var cli = Nash.createCli({ 32 | title: title, 33 | description: description, 34 | color: 'yellow', 35 | 36 | api: Divshot.createClient({ 37 | token: user.get('token'), 38 | host: API_HOST, 39 | client_id: CLIENT_ID 40 | }), 41 | 42 | user: user, 43 | cwd: cwd, 44 | errors: errors, 45 | environments: environments, 46 | timeout: 30000 47 | }); 48 | 49 | // Set up process watching dumping machine 50 | dumper(cli.api.events); 51 | 52 | // Flags 53 | cli.flag('-a', '--app') 54 | .description('override the directory\'s app name') 55 | .handler(function (appName) { 56 | cwd.overrideAppName(appName); 57 | }); 58 | 59 | cli.flag('-t', '--token') 60 | .description('override your current user authentication token') 61 | .handler(function (token) { 62 | cli.user.set('token', token, false); 63 | cli.api.setToken(token); 64 | }); 65 | 66 | cli.flag('-v', '--version') 67 | .description('show the CLI version number') 68 | .exit(true) 69 | .handler(function () { 70 | 71 | var package = require(path.resolve(__dirname, './package.json')); 72 | cli.log(package.version); 73 | }); 74 | 75 | cli.flag('-c', '--config') 76 | .description('use a different config file') 77 | .handler(function (filename) { 78 | cli.cwd.setConfigFilename(filename) 79 | }); 80 | 81 | cli.flag('--timeout') 82 | .description('set command timeout, in milliseconds') 83 | .handler(function (timeout) { 84 | cli.timeout = timeout; 85 | }); 86 | 87 | cli.flag('-o', '--org') 88 | .description('set command to operate on the given organization') 89 | .handler(function (org) { 90 | cli.org = org; 91 | }); 92 | 93 | // Helpers 94 | cli.method('authenticate', function (cli, command, done) { 95 | if (!cli.user.authenticated()) return done(cli.errors.NOT_AUTHENTICATED); 96 | done(); 97 | }); 98 | 99 | cli.method('isApp', function (cli, command, next) { 100 | if(!cli.cwd.getConfig().name) return next(cli.errors.DIRECTORY_NOT_APP); 101 | next(); 102 | }); 103 | 104 | cli.catchAll(function (type, attemptedCommand) { 105 | // Undefined command 106 | if (!attemptedCommand) return cli.commands.help({debug: true}); 107 | 108 | cli.log(); 109 | cli.log(format.bold('"' + attemptedCommand + '"') + ' is not a Divshot ' + type + '.'); 110 | cli.log('Please use ' + format.bold('"divshot help"') + ' for a list of Divshot commands.'); 111 | cli.log(); 112 | cli.log('Note: you may need to update Divshot in order to use that command: ' + format.bold('npm install -g divshot-cli')); 113 | }); 114 | 115 | // Add commands 116 | commands.connect(cli); 117 | 118 | module.exports = cli; 119 | -------------------------------------------------------------------------------- /lib/commands/account.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | var moment = require('moment'); 3 | var request = require('request'); 4 | var _ = require('lodash'); 5 | var formatUserEmails = require('../helpers/format_user_emails'); 6 | 7 | module.exports = function (cli) { 8 | var account = cli.command('account') 9 | .description('display basic account details') 10 | .before('authenticate') 11 | .handler(account); 12 | 13 | account.task('redeem ') 14 | .description('reedem a voucher and credit it to your account') 15 | .handler(redeem); 16 | 17 | function account (done) { 18 | cli.api.user.self(function (err, response, user) { 19 | if (err) return done(cli.errors.DEFAULT); 20 | 21 | var balance = (user.account.balance/100 < 0) ? 0 : user.account.balance; 22 | var output = {}; 23 | 24 | cli.log(); 25 | 26 | output['Account Balance'] = '$' + balance; 27 | 28 | // Account credit 29 | if (user.account.credit > 0) output['Account Credit'] = '$' + user.account.credit/100; 30 | 31 | output['Account Created'] = moment(user.created_at).format('MMMM Do, YYYY'); 32 | 33 | // Emails 34 | if (user.emails.length) output['Emails'] = formatUserEmails(user.emails); 35 | 36 | // Organizations 37 | if (user.organizations.length > 0) output['Organizations'] = _.pluck(user.organizations, 'name'); 38 | 39 | // Output to stdout 40 | cli.logObject(output); 41 | 42 | done(null, user); 43 | }); 44 | } 45 | 46 | function redeem (code, done) { 47 | if (!code) return done(cli.errors.INVALID_VOUCHER); 48 | 49 | cli.api.vouchers.redeem(code, function (err, response, body) { 50 | if (err) return done(cli.errors.DEFAULT); 51 | if (response.statusCode == 304) return done(cli.errors.VOUCHER_USED_BY_YOU); 52 | if (response.statusCode == 404) return done(cli.errors.INVALID_VOUCHER); 53 | if (response.statusCode == 403) return done(cli.errors.VOUCHER_USED); 54 | 55 | cli.log(); 56 | cli.log(format.bold('$' + body.amount/100) + ' has been applied to your account.', {success:true}); 57 | }); 58 | } 59 | 60 | }; -------------------------------------------------------------------------------- /lib/commands/apps.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | var _ = require('lodash'); 3 | 4 | module.exports = function (cli) { 5 | cli.command('apps') 6 | .description('list your Divshot apps') 7 | .before('authenticate') 8 | .handler(function (done) { 9 | 10 | cli.api.apps.list(function (err, apps) { 11 | 12 | cli.log(); 13 | 14 | if (apps && apps.error === 'invalid_token') return done(cli.errors.INVALID_TOKEN); 15 | if (apps && apps.error) return done(apps.error); 16 | if (!apps || !apps.self) return done(cli.errors.DEFAULT); 17 | 18 | cli.log(format.yellow(' Your apps\n')); 19 | 20 | printApps(apps.self); 21 | printOrgApps(apps); 22 | 23 | done(err, apps); 24 | }); 25 | }); 26 | 27 | function printOrgApps (apps) { 28 | _(apps) 29 | .keys() 30 | .filter(function (name) { 31 | return name.split(':')[0] === 'org'; 32 | }) 33 | .each(printOrgTitle); 34 | 35 | function printOrgTitle (name) { 36 | var type = name.split(':')[0] 37 | var orgName = name.split(':')[1] 38 | 39 | cli.log(); 40 | cli.log(format.yellow(' ' + orgName + ' apps')); 41 | cli.log(); 42 | 43 | printEachOrgApp(apps[name]); 44 | } 45 | } 46 | 47 | function printEachOrgApp (orgApps) { 48 | if (!orgApps.length) return cli.log(' (no apps)'); 49 | printApps(orgApps); 50 | } 51 | 52 | function printApps (apps) { 53 | _(apps) 54 | .map(function (app) { 55 | return ' ' + (app.name || app.architect.filename); 56 | }) 57 | .sortBy() 58 | .each(cli.log.bind(cli)); 59 | } 60 | }; -------------------------------------------------------------------------------- /lib/commands/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = function (cli) { 2 | cli.command('auth') 3 | .description('authentication help') 4 | .task('token') 5 | .description('show your authentication token') 6 | .handler(function (done) { 7 | var token = cli.user.get('token'); 8 | cli.log(token); 9 | done(null, token); 10 | }); 11 | }; -------------------------------------------------------------------------------- /lib/commands/cert.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var resolvePath = require('path').resolve; 3 | 4 | var format = require('chalk'); 5 | 6 | var use = 'cert [cabundlefile]'; 7 | 8 | module.exports = function (cli) { 9 | 10 | var command = cli.command(use); 11 | var config = cli.cwd.getConfig(); 12 | 13 | command.before('authenticate', 'isApp'); 14 | command.description('upload an ssl certificate for your app'); 15 | 16 | command.handler(function (certificateFile, keyFile, cabundleFile, done) { 17 | 18 | if(!(certificateFile && keyFile)) { 19 | cli.log(); 20 | return done('To upload a certificate, you must provide both the certificate and file paths.\n\n Example: ' + format.bold(use)); 21 | } 22 | 23 | var requireCabundle = !!cabundleFile; 24 | var hasCert = requirePath(certificateFile, 'Certificate'); 25 | var hasKey = requirePath(keyFile, 'Key'); 26 | var hasCabundle = (!requireCabundle || requirePath(cabundleFile, 'CA Bundle')); 27 | 28 | if(!(hasCert && hasKey && hasCabundle)) { 29 | return done('Missing files.'); 30 | } 31 | 32 | var appApi = cli.api.apps.id(config.name); 33 | var url = appApi.url() + '/ssl'; 34 | var payload = { 35 | ssl_cert: fs.readFileSync(certificateFile).toString(), 36 | ssl_key: fs.readFileSync(keyFile).toString() 37 | }; 38 | 39 | if (requireCabundle) { 40 | payload.ssl_bundle = fs.readFileSync(cabundleFile).toString(); 41 | } 42 | 43 | cli.log("Uploading certificate..."); 44 | 45 | appApi.http.request(url, 'PUT', {form: payload}, function(err, response) { 46 | 47 | cli.log(); 48 | 49 | if (err) { 50 | return done(err.message); 51 | } 52 | 53 | if (response.statusCode == 400) { 54 | return done(cli.errors.INVALID_CERT); 55 | } 56 | 57 | if (response.statusCode == 401) { 58 | return done(cli.errors.NOT_ADMIN); 59 | } 60 | 61 | if (response.statusCode >= 400) { 62 | return done(cli.errors.APP_NOT_PRODUCTION); 63 | } 64 | 65 | if (response.message) { 66 | return done(response.message); 67 | } 68 | 69 | cli.log("SSL certificate uploaded.", {success:true}); 70 | cli.log(); 71 | }); 72 | }); 73 | 74 | function requirePath(path, name) { 75 | 76 | path = resolvePath(path); 77 | 78 | if(fs.existsSync(path)) { 79 | return true; 80 | } 81 | 82 | cli.log(format.red('Could not find the SSL ' + name + ' file at: ' + path)); 83 | 84 | return false; 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /lib/commands/changelog.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var request = require('request'); 3 | var marked = require('marked'); 4 | var TerminalRenderer = require('marked-terminal'); 5 | var format = require('chalk'); 6 | 7 | module.exports = function (cli) { 8 | var changelog = cli.command('changelog') 9 | .description('view info for the latest changes the the Divshot cli') 10 | .handler(function (done) { 11 | 12 | // Define custom renderer 13 | marked.setOptions({ 14 | renderer: new TerminalRenderer() 15 | }); 16 | 17 | var requestOptions = { 18 | uri: 'https://api.github.com/repos/divshot/divshot-cli/releases', 19 | method: 'GET', 20 | headers: {'user-agent': 'node.js'} 21 | }; 22 | 23 | request(requestOptions, function (err, response, body) { 24 | 25 | var releases = JSON.parse(body); 26 | 27 | var stream = require('stream'); 28 | var s = new stream.Readable(); 29 | s._read = function noop() {}; // redundant? see update below 30 | 31 | _.each(releases.reverse(), function (release) { 32 | 33 | var data = ''; 34 | data += '\n\n' + format.bold[cli.color].underline('=== ' + release.name); 35 | data += '\n\n' 36 | data += marked(release.body); 37 | 38 | s.push(data); 39 | }); 40 | 41 | s.push(null); 42 | 43 | s.pipe(process.stdout); 44 | }); 45 | }); 46 | }; -------------------------------------------------------------------------------- /lib/commands/config.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var format = require('chalk'); 3 | 4 | module.exports = function (cli) { 5 | var command = cli.command('config'); 6 | var config = cli.cwd.getConfig(); 7 | 8 | command.before('isApp'); 9 | command.description('edit keys and values from your app config file'); 10 | command.handler(function (key, done) { 11 | cli.log(); 12 | 13 | if (key) { 14 | var val = config[key]; 15 | 16 | try { 17 | val = JSON.stringify(val, null, 2); 18 | } 19 | catch (e) {} 20 | 21 | cli.log(format.yellow(key) + ': ' + val); 22 | } 23 | else { 24 | cli.log(format.yellow('app config\n')); 25 | cli.log(JSON.stringify(config, null, 2)); 26 | } 27 | 28 | done(null, config); 29 | }); 30 | 31 | var add = command.task('add '); 32 | var remove = command.task('remove '); 33 | 34 | add.description('add a value to the config file'); 35 | remove.description('remove a value from the config file'); 36 | 37 | add.handler(function (key, value, done) { 38 | if (!value) return done(cli.errors.MISSING_CONFIG_VALUE); 39 | 40 | cli.cwd.setConfigValue(key, value); 41 | cli.log('\nConfig value added'); 42 | done(null, cli.cwd.getConfig()); 43 | }); 44 | 45 | remove.handler(function (key, done) { 46 | if (!key) return done(cli.errors.MISSING_CONFIG_KEY); 47 | cli.cwd.removeConfigValue(key); 48 | cli.log('\n' + format.bold(key) + ' removed'); 49 | done(null, cli.cwd.getConfig()); 50 | }); 51 | }; -------------------------------------------------------------------------------- /lib/commands/create.js: -------------------------------------------------------------------------------- 1 | var promptly = require('promptly'); 2 | var format = require('chalk'); 3 | var regular = require('regular'); 4 | 5 | module.exports = function (cli) { 6 | 7 | var command = cli.command('create '); 8 | 9 | command.before('authenticate'); 10 | command.description('create a new app'); 11 | 12 | command.handler(function (name, done) { 13 | 14 | cli.log(); 15 | 16 | if (cli.org) { 17 | cli.log('Creating app in organization ' + format.bold(cli.org) + ' ...'); 18 | } 19 | else { 20 | cli.log('Creating app ...'); 21 | } 22 | 23 | parseAppname(name, cli.cwd.getConfig().name, function (err, name) { 24 | 25 | if (err) return done(err); 26 | 27 | var apps = cli.api.apps; 28 | var appParams = name.toLowerCase(); 29 | 30 | if (cli.org) { 31 | apps = cli.api.organizations.id(cli.org).apps; 32 | appParams = {name: name.toLowerCase()}; 33 | } 34 | 35 | apps.create(appParams, function (err, response, body) { 36 | 37 | if (err) return done(err); 38 | if (response.error === 'invalid_token') return done(cli.errors.INVALID_TOKEN + ' ' + cli.errors.NOT_AUTHENTICATED); 39 | if (response.error) return done(response.error); 40 | if (body && body.error) return done(body.error + ' ' + cli.errors.NOT_AUTHENTICATED); 41 | if (response.statusCode >= 400) return done(body.error + ' ' + cli.errors.NOT_AUTHENTICATED); 42 | 43 | cli.log(format.bold(name) + ' has been created', {success: true}); 44 | done(null, body); 45 | }); 46 | }); 47 | }); 48 | 49 | function parseAppname (name, configName, callback) { 50 | 51 | if (name) return callback(null, name); 52 | if (configName) return callback(null, configName); 53 | 54 | promptly.prompt('App name: ', { 55 | trim: true, 56 | validator: function (name) { 57 | 58 | return (regular.slug.test(name)) ? name : false; 59 | } 60 | }, function (err, appName) { 61 | 62 | if (!appName) return callback(cli.errors.INVALID_APP_NAME); 63 | callback(null, appName); 64 | }); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /lib/commands/destroy.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | var promptly = require('promptly'); 3 | var _ = require('lodash'); 4 | 5 | module.exports = function (cli) { 6 | var command = cli.command('destroy ', 'delete '); 7 | 8 | command.before('authenticate'); 9 | command.description('delete an app'); 10 | 11 | command.handler(function (appName, done) { 12 | if (!appName) return done(cli.errors.MISSING_APP_NAME); 13 | 14 | promptly.confirm('Are you sure you want to permanantly delete this app? (y/n)', function (err, confirmed) { 15 | if (!confirmed) return done(); 16 | 17 | cli.commands.apps(function (err, apps) { 18 | var appObj = _.find(apps.self, function (app) { 19 | return app.name === appName; 20 | }); 21 | 22 | if (!appObj) return done(format.bold(appName) + ' app does not exist'); 23 | 24 | cli.api.apps.one(appObj.name).remove(function (err, response) { 25 | if (err) return done(cli.errors.DEFAULT); 26 | if (response.status == 404) return done(response.error); 27 | 28 | cli.log(format.bold(appName) + ' has been permanantly deleted.', {success: true}); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | }); 34 | }; -------------------------------------------------------------------------------- /lib/commands/domains.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | var _ = require('lodash'); 3 | 4 | module.exports = function (cli) { 5 | 6 | var command = cli.command('domains', 'dom'); 7 | 8 | command.before('authenticate'); 9 | command.description('list, add, and remove custom domains for your apps'); 10 | command.handler(function (done) { 11 | 12 | var appName = cli.cwd.getConfig().name; 13 | 14 | cli.api.apps.id(appName).domains.list(function (err, response) { 15 | 16 | cli.log(); 17 | 18 | // Error in request 19 | if (response.error && response.status >= 400) { 20 | 21 | // Not allowed to change domain settings 22 | // on this app 23 | if (response.status == 401) { 24 | return done(response.error); 25 | } 26 | 27 | // Need to upgrade 28 | if (response.status == 402) { 29 | return done(cli.errors.UPGRADE_FOR_NON_PROD_DOMAINS); 30 | } 31 | 32 | return done(response.error); 33 | } 34 | 35 | if (response.error) { 36 | return done(cli.errors.NOT_ADMIN); 37 | } 38 | 39 | // No custom domains 40 | if (response.length < 1) { 41 | cli.log('You don\'t have any custom domains for ' + format.bold(appName) + '.\nUse ' + format.bold('divshot domains:add [www.domain.com]') + ' to add a domain.'); 42 | return done(null, []); 43 | } 44 | 45 | var domains = response; 46 | 47 | cli.log('Custom domains for ' + format.bold(appName) + ':\n'); 48 | 49 | domains.forEach(function (domain) { 50 | 51 | cli.log(' ' + format.bold(domain.name) + ' (' + domain.env + ')'); 52 | }); 53 | 54 | done(null, domains); 55 | }); 56 | }); 57 | 58 | var add = command.task('add '); 59 | var remove = command.task('remove '); 60 | 61 | add.description('add a custom domain to the app'); 62 | remove.description('remove a custom domain from the app'); 63 | 64 | add.handler(function (domain, done) { 65 | 66 | // Ensure domain is added to correct environment 67 | var args = cli.args.args; 68 | var environment = 'production'; 69 | if (args.length > 1) { 70 | environment = args[0]; 71 | domain = args[1]; 72 | } 73 | 74 | var appName = cli.cwd.getConfig().name; 75 | 76 | cli.api.apps.id(appName).domains.add(domain, {env: environment}, function (err, response, body) { 77 | 78 | if (err) { 79 | return done(cli.errors.DEFAULT); 80 | } 81 | if (response.statusCode == 402) { 82 | var upgradeMessage = cli.errors.UPGRADE_TO_USE_FEATURE + '\n\nVisit ' + format.bold('https://dashboard.divshot.com/apps/' + appName + '/settings') + ' to upgrade your app.'; 83 | return done(upgradeMessage); 84 | } 85 | if (+response.statusCode >= 400) { 86 | return done(JSON.parse(response.body).error); 87 | } 88 | 89 | cli.log(); 90 | cli.log(format.bold(domain) + ' has been added to ' + format.bold(appName) + ' on the ' + format.bold(environment) + ' environment.', {success: true}); 91 | cli.log('See ' + format.bold('http://docs.divshot.io/guides/domains') + ' for details on how to configure DNS for your domain.'); 92 | 93 | var data = JSON.parse(response.body); 94 | 95 | if (data.apex) { 96 | cli.log(''); 97 | cli.log('You have added an Apex domain to your application.'); 98 | cli.log('Be sure to check the documentation for extra details about Apex domain configuration.'); 99 | } 100 | 101 | done(); 102 | }); 103 | }); 104 | 105 | remove.handler(function (domain, done) { 106 | 107 | var appName = cli.cwd.getConfig().name; 108 | 109 | cli.api.apps.id(appName).domains.remove(domain, function (err, response) { 110 | 111 | cli.log(); 112 | 113 | if (err || +response.statusCode !== 200) { 114 | return done(cli.errors.DEFAULT); 115 | } 116 | 117 | cli.log(format.bold(domain) + ' has been removed from ' + format.bold(appName), {success: true}); 118 | done(); 119 | }); 120 | }); 121 | }; -------------------------------------------------------------------------------- /lib/commands/dump.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var bundleFiles = require('../helpers/bundle_files'); 3 | var path = require('path'); 4 | 5 | module.exports = function(cli) { 6 | var command = cli.command('dump'); 7 | command.description('dump a .tar.gz of your project as it would be uploaded. (useful for diagnostics)'); 8 | command.secret(true); 9 | command.handler(function(environment, done) { 10 | var config = cli.cwd.getConfig(); 11 | // var appConfigRootPath = (config.root === '/') ? './' : config.root; 12 | // var appRootDir = path.resolve(process.cwd(), appConfigRootPath); 13 | 14 | // if (!fs.existsSync(appRootDir)) return done(cli.errors.DIRECTORY_DOES_NOT_EXIST); 15 | 16 | // var dumpLocation = config.name + '-dump.tar.gz'; 17 | // var dumpStream = fs.createWriteStream(dumpLocation); 18 | 19 | // bundleFiles(appRootDir, config.exclude) 20 | // .pipe(dumpStream) 21 | // .on('error', function(error) { 22 | // done(error); 23 | // }) 24 | // .on('close', function() { 25 | // cli.log('Dumped Project to ' + dumpLocation, {success: true}); 26 | // done(); 27 | // }); 28 | 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/commands/emails.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var regular = require('regular'); 3 | var formatUserEmails = require('../helpers/format_user_emails'); 4 | 5 | module.exports = function (cli) { 6 | var emails = cli.command('emails') 7 | .description('show all emails associated with your account') 8 | .before('authenticate') 9 | .handler(showUserEmails); 10 | 11 | emails.task('add ') 12 | .description('add an email to your account') 13 | .handler(addEmail); 14 | 15 | emails.task('remove ') 16 | .description('remove an email from your account') 17 | .handler(removeEmail); 18 | 19 | emails.task('resend ') 20 | .description('resend the confirmation for an email') 21 | .handler(resend); 22 | 23 | function showUserEmails (done) { 24 | cli.api.user.self(function (err, response, user) { 25 | if (err) return done(cli.errors.DEFAULT); 26 | 27 | cli.log(); 28 | cli.logObject(formatUserEmails(user.emails), { 29 | leftPadding: 2 30 | }); 31 | 32 | done(null, user.emails); 33 | }); 34 | } 35 | 36 | function addEmail (email, done) { 37 | if (!email) return done(cli.errors.MISSING_EMAIL); 38 | if (!regular.email.test(email)) return done(cli.errors.INVALID_EMAIL); 39 | 40 | cli.api.user.emails.add(email, function (err, response, body) { 41 | if (err || response.statusCode >= 300) return done(cli.errors.DEFAULT); 42 | 43 | cli.log(); 44 | cli.log(email + ' has been added to your account', {success: true}); 45 | 46 | done(null, body); 47 | }); 48 | } 49 | 50 | function removeEmail (email, done) { 51 | if (!email) return done(cli.errors.MISSING_EMAIL); 52 | 53 | cli.api.user.emails.remove(email, function (err, response, body) { 54 | if (err || response.statusCode >= 300) return done(cli.errors.DEFAULT); 55 | 56 | cli.log(); 57 | cli.log(email + ' has been removed from your account', {success: true}); 58 | 59 | done(null, body); 60 | }); 61 | } 62 | 63 | function resend(email, done) { 64 | if (!email) return done(cli.errors.MISSING_EMAIL); 65 | 66 | cli.api.user.emails.resend(email, function (err, response, body) { 67 | if (err || response.statusCode >= 300) return done(cli.errors.DEFAULT); 68 | 69 | cli.log(); 70 | cli.log('A confirmation email has been resent to ' + email, {success: true}); 71 | done(null, body); 72 | }); 73 | } 74 | }; -------------------------------------------------------------------------------- /lib/commands/env.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var regular = require('regular'); 3 | var format = require('chalk'); 4 | var _ = require('lodash'); 5 | var util = require('util'); 6 | var booly = require('booly'); 7 | 8 | module.exports = function (cli) { 9 | 10 | function withApp(callback) { 11 | 12 | return function (environment, done) { 13 | callback(cli.api.apps.id(cli.cwd.getConfig().name), environment, done); 14 | } 15 | } 16 | 17 | var command = cli.command('env'); 18 | 19 | command.before('authenticate'); 20 | command.description('work with environment variables'); 21 | command.handler(withApp(function (app, environment, done) { 22 | 23 | app.endpoint('env').one(environment + '/config').get(function(err, response) { 24 | 25 | cli.log(); 26 | 27 | if (!environment) { 28 | return done(cli.errors.MISSING_ENVIRONMENT); 29 | } 30 | 31 | if (err) { 32 | return done(cli.errors.DEFAULT); 33 | } 34 | 35 | if (_.isEqual(response, {})) { 36 | cli.log(format.bold(environment) + ' does not have any environment variables'); 37 | return done(); 38 | } 39 | 40 | if (response.status == 404) { 41 | return done(cli.errors.DIRECTORY_NOT_APP); 42 | } 43 | 44 | cli.logObject(response); 45 | done(null, response); 46 | }); 47 | })); 48 | 49 | // // Configure environment (i.e. force ssl, etc.) 50 | // command.task('config ') 51 | // .hidden() 52 | // .description('configure your app\'s environment') 53 | // .handler(function (environment, done) { 54 | 55 | // cli.log(); 56 | // if (!environment) return done(cli.errors.MISSING_ENVIRONMENT); 57 | 58 | // var app = cli.api.apps.id(cli.cwd.getConfig().name); 59 | // var settings = parseConfig(cli.args.args); 60 | 61 | // app.env(environment).config(settings, function (err, response) { 62 | 63 | // if (err || response.statusCode != 200) return done(cli.errors.DEFAULT); 64 | // if (response.error) return done(response.error); 65 | 66 | // cli.log(format.bold(environment) + ' has been updated with:\n', {success: true}); 67 | // cli.logObject(settings); 68 | 69 | // done(); 70 | // }); 71 | 72 | // function parseConfig (args) { 73 | 74 | // return _(args) 75 | // .filter(function (arg) { 76 | 77 | // if (arg === environment) return false; 78 | // return true; 79 | // }) 80 | // .map(function (arg) { 81 | 82 | // var setting = arg.split('='); 83 | // setting[1] = booly(setting[1]); 84 | 85 | // return setting; 86 | // }) 87 | // .zipObject() 88 | // .value(); 89 | // } 90 | // }); 91 | 92 | // // Create 93 | // command.task('create ') 94 | // .description('create a custom-named environment') 95 | // .handler(withApp(function(app, environment, done) { 96 | // var endpoint = app.endpoint('env').one(environment); 97 | // endpoint.http.request(endpoint.url(), 'PUT', function(err, response) { 98 | // if (err) return done(err.message); 99 | // if (response.statusCode !== 200) { 100 | // done(response.message); 101 | // } 102 | // else { 103 | // cli.log('Created environment ' + environment); 104 | // done(); 105 | // } 106 | // }); 107 | // })); 108 | 109 | // // Delete 110 | // command.task('delete ') 111 | // .description('delete a named environment') 112 | // .handler(withApp(function(app, environment, done) { 113 | // var endpoint = app.endpoint('env').one(environment); 114 | // endpoint.http.request(endpoint.url(), 'DELETE', function(err, response) { 115 | // if(err) return done(err.message); 116 | // if (response.statusCode !== 200) { 117 | // done(response.message); 118 | // } 119 | // else { 120 | // cli.log('Deleted environment ' + environment); 121 | // done(); 122 | // } 123 | // }); 124 | // })); 125 | 126 | // Pull 127 | command.task('pull ') 128 | .description('copy environment data to your local environment') 129 | .handler(withApp(function (app, environment, done) { 130 | 131 | app.endpoint('env').one(environment + '/config').get(function(err, response) { 132 | 133 | if (err) return done(cli.errors.DEFAULT); 134 | 135 | try { 136 | var currentConfig = JSON.parse(fs.readFileSync('./.env.json')); 137 | } 138 | catch (error) { 139 | var currentConfig = {}; 140 | } 141 | 142 | var skipped = {}; 143 | 144 | Object.keys(response).forEach(function (key) { 145 | 146 | if (currentConfig[key]) { 147 | if (currentConfig[key] != response[key]) { 148 | skipped[key] = response[key]; 149 | } 150 | return; 151 | } 152 | currentConfig[key] = response[key]; 153 | }); 154 | 155 | if (_.any(skipped)) { 156 | cli.log('Skipping values that would clobber environment on disk!', {warning: true}); 157 | Object.keys(skipped).forEach(function (key) { 158 | 159 | cli.log(key.bold + ': ' + skipped[key]); 160 | }); 161 | 162 | } 163 | 164 | fs.writeFileSync('./.env.json', JSON.stringify(currentConfig, null, 2)); 165 | cli.log('Wrote config to .env.json', {success: true}); 166 | done(err, response); 167 | }); 168 | })); 169 | 170 | // Add 171 | command.task('add KEY=value KEY2=value ...') 172 | .description('add environment data') 173 | .handler(withApp(function (app, environment, done) { 174 | 175 | var vars = {}; 176 | var match; 177 | 178 | cli.args.args.forEach(function(arg) { 179 | 180 | match = arg.match(/([\w_-]+)=(.*)?/) 181 | if (match) { 182 | vars[match[1]] = arg.slice(match[1].length + 1); 183 | } 184 | }); 185 | 186 | app.env(environment).config({ env: vars }, function(err, response) { 187 | 188 | cli.log(); 189 | 190 | if (err) return done(cli.errors.DEFAULT); 191 | if (response.statusCode == 400) return done(cli.errors.MUST_HAVE_DEPLOYED_APP); 192 | if (response.statusCode !== +200) return done(cli.errors.DEFAULT); 193 | 194 | cli.log(format.bold(environment) + ' has been configured..', {success: true}); 195 | 196 | Object.keys(vars).forEach(function(variable) { 197 | 198 | cli.log(variable, {success: true}); 199 | }); 200 | 201 | done(err, response); 202 | }); 203 | })); 204 | 205 | // TODO: implement this into the API 206 | command.task('remove KEY1 Key2 ...') 207 | .description('remove environment data') 208 | .handler(withApp(function (app, environment, done) { 209 | 210 | if (!environment) { 211 | return done(cli.errors.MISSING_ENVIRONMENT); 212 | } 213 | 214 | var vars = _(cli.args.args) 215 | .rest() 216 | .map(function (key) { 217 | 218 | return [key, null] 219 | }) 220 | .zipObject() 221 | .value(); 222 | 223 | app.env(environment).config({ env: vars }, function(err, response) { 224 | 225 | cli.log(); 226 | 227 | if (err) { 228 | return done(cli.errors.DEFAULT); 229 | } 230 | 231 | if (response.statusCode == 400) { 232 | return done(cli.errors.MUST_HAVE_DEPLOYED_APP); 233 | } 234 | 235 | if (response.statusCode !== +200) { 236 | return done(cli.errors.DEFAULT); 237 | } 238 | 239 | cli.log('Values have been removed from the environment for ' + format.bold(environment), {success: true}); 240 | 241 | done(err, response); 242 | }); 243 | })); 244 | }; 245 | -------------------------------------------------------------------------------- /lib/commands/files.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var format = require('chalk'); 3 | 4 | module.exports = function (cli) { 5 | 6 | cli.command('files ') 7 | .description('show a list of the files for the given environment') 8 | .before('authenticate') 9 | .handler(function (environment, done) { 10 | 11 | if (!environment) return done(cli.errors.MISSING_ENVIRONMENT); 12 | 13 | var appName = cli.cwd.getConfig().name; 14 | 15 | if (!appName) return done(cli.errors.DIRECTORY_NOT_APP); 16 | 17 | var app = cli.api.apps.id(appName); 18 | 19 | app.releases.env(environment).get().then(function (releases) { 20 | 21 | var lastRelease = _(releases) 22 | .sortBy(version) 23 | .last(); 24 | 25 | var files = fileList(lastRelease); 26 | 27 | cli.log(); 28 | cli.logObject(files, {leftPadding: 2}); 29 | 30 | done(null, files); 31 | }); 32 | 33 | function fileList (release) { 34 | 35 | var files = (release.build.file_mapped) 36 | ? hashedFilelist(release) 37 | : nonHashedFileList(release) 38 | 39 | return _.sortBy(files, function (name) { 40 | return name.toLowerCase(); 41 | }); 42 | } 43 | 44 | function hashedFilelist (release) { 45 | 46 | return _.keys(release.build.file_map); 47 | } 48 | 49 | function nonHashedFileList (release) { 50 | 51 | return release.build.files; 52 | } 53 | 54 | function version (release) { 55 | 56 | return release.version; 57 | } 58 | }); 59 | }; -------------------------------------------------------------------------------- /lib/commands/hooks.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var format = require('chalk'); 3 | var regular = require('regular'); 4 | var inquirer = require('inquirer'); 5 | 6 | module.exports = function (cli) { 7 | 8 | var hooks = cli.command('hooks') 9 | .description('get a list of webhooks for your app') 10 | .before('isApp', 'authenticate') 11 | .handler(listHooks); 12 | 13 | hooks.task('add ') 14 | .description('add a webhook to your app') 15 | .handler(addHook); 16 | 17 | hooks.task('remove ') 18 | .description('remove a webhook from your app') 19 | .handler(removeHook); 20 | 21 | hooks.task('pause ') 22 | .description('deactivate a webhook without removing it') 23 | .handler(activateHook(false)); 24 | 25 | hooks.task('resume ') 26 | .description('reactivate a paused webhook') 27 | .handler(activateHook(true)); 28 | 29 | function listHooks (done) { 30 | 31 | var localApp = cli.cwd.getConfig(); 32 | var app = cli.api.apps.id(localApp.name); 33 | 34 | app.webhooks.list(function (err, hooks) { 35 | 36 | if (err) { 37 | return done(err.message); 38 | } 39 | 40 | if (hooks.length < 1) { 41 | cli.log('\nYour app doesn\'t have any webhooks yet. Use ' + format.bold('divshot hooks:add ') + ' to an a webhook.'); 42 | } 43 | 44 | var active = sortHooks(hooks, 'active'); 45 | var inactive = sortHooks(hooks, 'inactive'); 46 | 47 | cli.log(); 48 | 49 | cli.logObject(hooks.map(describeHook), { 50 | leftPadding: 2 51 | }); 52 | }); 53 | } 54 | 55 | function addHook (url, done) { 56 | 57 | cli.log(); 58 | 59 | if (!regular.url.test(url)) { 60 | done(cli.errors.INVALID_URL); 61 | } 62 | 63 | var localApp = cli.cwd.getConfig(); 64 | 65 | cli.api.apps.id(localApp.name).webhooks.create({ 66 | url: url, 67 | active: true 68 | }, function (err, response) { 69 | 70 | if (err) { 71 | done(err.message); 72 | } 73 | 74 | if (response.error) { 75 | return done(response.error); 76 | } 77 | 78 | cli.log('The webhook ' + format.bold(url) + ' has been added and activated', {success: true}); 79 | }); 80 | } 81 | 82 | function removeHook (url, done) { 83 | 84 | var localApp = cli.cwd.getConfig(); 85 | var appHooks = cli.api.apps.id(localApp.name).webhooks; 86 | 87 | appHooks.list(function (err, hooks) { 88 | 89 | cli.log(); 90 | 91 | if (err) { 92 | return done(err.message); 93 | } 94 | 95 | if (hooks.error) { 96 | return done(hooks.error); 97 | } 98 | 99 | if (hooks.length < 1) { 100 | cli.log('Your app does not have any webhooks.'); 101 | return; 102 | } 103 | 104 | // User provided a url in the command call, so it 105 | // gets removed here 106 | if (url) { 107 | return removeHook(hooks, url); 108 | } 109 | 110 | // Ask user to choose a webhook from a list if the user 111 | // doesn't provide a url in the command call 112 | else { 113 | inquirer.prompt({ 114 | type: 'list', 115 | name: 'hook', 116 | message: 'Which hook would you like to remove?', 117 | choices: _.pluck(hooks, 'url') 118 | }, function(answer) { 119 | 120 | removeHook(hooks, answer.hook); 121 | }); 122 | } 123 | }); 124 | 125 | function removeHook (hooks, url) { 126 | 127 | var hook = _.find(hooks, function (hook) { 128 | 129 | return hook.url === url; 130 | }); 131 | 132 | if (!hook) { 133 | return done(cli.errors.INVALID_HOOK_ID); 134 | } 135 | 136 | cli.log('Removing webhook ...'); 137 | 138 | appHooks.remove(hook.id, function (err, response) { 139 | 140 | cli.log(); 141 | 142 | if (err) { 143 | return done(err.message); 144 | } 145 | 146 | if (response.statusCode !== 204) { 147 | return done(cli.errors.INVALID_HOOK_ID); 148 | } 149 | 150 | cli.log(format.bold(url) + ' has been removed.', {success: true}); 151 | }); 152 | } 153 | } 154 | 155 | function activateHook (shouldActivate) { 156 | 157 | var activationVerb = shouldActivate ? 'Resuming' : 'Pausing'; 158 | var activationAction = shouldActivate ? 'resume': 'pause'; 159 | 160 | return function (url, done) { 161 | 162 | var localApp = cli.cwd.getConfig(); 163 | var appHooks = cli.api.apps.id(localApp.name).webhooks; 164 | 165 | appHooks.list(function (err, hooks) { 166 | 167 | cli.log(); 168 | 169 | if (err) { 170 | return done(err.message); 171 | } 172 | 173 | if (hooks.error) { 174 | return done(hooks.error); 175 | } 176 | 177 | if (hooks.length < 1) { 178 | cli.log('Your app does not have any webhooks.'); 179 | return; 180 | } 181 | 182 | // User provided a url in the command call, so it 183 | // gets removed here 184 | if (url) { 185 | return handleActivation(hooks, url); 186 | } 187 | 188 | // Ask user to choose a webhook from a list if the user 189 | // doesn't provide a url in the command call 190 | else { 191 | inquirer.prompt({ 192 | type: 'list', 193 | name: 'hook', 194 | message: 'Which hook would you like to ' + activationAction + '?', 195 | choices: _.pluck(hooks, 'url') 196 | }, function(answer) { 197 | 198 | handleActivation(hooks, answer.hook); 199 | }); 200 | } 201 | }); 202 | 203 | function handleActivation (hooks, url) { 204 | 205 | var hook = _.find(hooks, function (hook) { 206 | 207 | return hook.url === url; 208 | }); 209 | 210 | if (!hook) { 211 | return done(cli.errors.INVALID_HOOK_ID); 212 | } 213 | 214 | cli.log(activationVerb + ' webhook ...'); 215 | 216 | appHooks[activationAction](hook, function (err, hook) { 217 | 218 | cli.log(); 219 | 220 | if (err) { 221 | return done(err.message); 222 | } 223 | 224 | if (hook.statusCode) { 225 | return done(cli.errors.INVALID_HOOK_ID); 226 | } 227 | 228 | cli.log(format.bold(url) + ' has been ' + activationAction + 'd.', {success: true}); 229 | }); 230 | } 231 | } 232 | } 233 | 234 | }; 235 | 236 | function sortHooks (hooks, status) { 237 | 238 | return _(hooks) 239 | .filter({active: (status === 'active') ? true : false}) 240 | .map(describeHook) 241 | .value(); 242 | } 243 | 244 | function describeHook (hook) { 245 | 246 | var desc = hook.url; 247 | 248 | if (!hook.active) { 249 | desc += ' (inactive)'; 250 | } 251 | 252 | return desc; 253 | } -------------------------------------------------------------------------------- /lib/commands/index.js: -------------------------------------------------------------------------------- 1 | var commands = { 2 | account: require('./account'), 3 | apps: require('./apps'), 4 | auth: require('./auth'), 5 | changelog: require('./changelog'), 6 | config: require('./config'), 7 | create: require('./create'), 8 | destroy: require('./destroy'), 9 | domains: require('./domains'), 10 | emails: require('./emails'), 11 | env: require('./env'), 12 | files: require('./files'), 13 | hooks: require('./hooks'), 14 | init: require('./init'), 15 | login: require('./login'), 16 | logout: require('./logout'), 17 | open: require('./open'), 18 | performance: require('./performance'), 19 | promote: require('./promote'), 20 | protect: require('./protect'), 21 | pull: require('./pull'), 22 | purge: require('./purge'), 23 | push: require('./push'), 24 | rename: require('./rename'), 25 | request: require('./request'), 26 | rollback: require('./rollback'), 27 | server: require('./server'), 28 | stats: require('./stats'), 29 | status: require('./status'), 30 | unprotect: require('./unprotect'), 31 | dump: require('./dump'), 32 | cert: require('./cert') 33 | }; 34 | 35 | exports.connect = function (cli) { 36 | Object.keys(commands).forEach(function (commandName) { 37 | commands[commandName](cli); 38 | }); 39 | }; -------------------------------------------------------------------------------- /lib/commands/init.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var promptly = require('promptly'); 4 | var slug = require('cozy-slug'); 5 | var async = require('async'); 6 | var slugValidator = require('../validators/slug'); 7 | var format = require('chalk'); 8 | var mkdirp = require('mkdirp'); 9 | var _ = require('lodash'); 10 | var copy = require('copy-files'); 11 | 12 | module.exports = function (cli) { 13 | cli.command('init ') 14 | .description('guide to initiate an app') 15 | .handler(function (directory, done) { 16 | if (directory) cli.cwd.setCwd(directory); 17 | 18 | var prompts = { 19 | overwrite: function (callback) { 20 | if (!cli.cwd.hasConfig()) return callback(null, true); 21 | 22 | promptly.confirm('Configuration file already exists. Overwrite?: (y/n)', function (err, overwrite) { 23 | if (overwrite) return callback(err, overwrite); 24 | process.exit(0); 25 | }); 26 | }, 27 | 28 | name: function (callback) { 29 | promptly.prompt('name: (' + currentDirName() + ')', { 30 | default: currentDirName(), 31 | validator: slugValidator 32 | }, callback); 33 | }, 34 | 35 | root: function (callback) { 36 | var defaultRootTitle = (directory) ? currentDirName() : 'current'; 37 | var defaultRoot = './'; 38 | promptly.prompt('root directory: (' + defaultRootTitle + ') ', { default: defaultRoot, }, callback); 39 | }, 40 | 41 | cleanUrls: function (callback) { 42 | promptly.confirm('clean urls: (y/n)', callback); 43 | }, 44 | 45 | errorPage: function (callback) { 46 | var defaultPageName = 'error.html'; 47 | promptly.prompt('error page: (' + defaultPageName + ')', { default: defaultPageName }, callback); 48 | }, 49 | 50 | createApp: function (callback) { 51 | promptly.confirm('Would you like to create an app on Divshot from this app?: (y/n)', callback); 52 | } 53 | }; 54 | 55 | async.series({ 56 | overwrite: prompts.overwrite, 57 | name: prompts.name, 58 | root: prompts.root, 59 | clean_urls: prompts.cleanUrls, 60 | error_page: prompts.errorPage, 61 | createApp: prompts.createApp 62 | }, createApplicationFiles); 63 | 64 | function createApplicationFiles(err, options) { 65 | var config = _.omit(options, ['overwrite', 'createApp']); 66 | 67 | mkdirp.sync(cli.cwd.cwd); // Create working app directory 68 | cli.cwd.setConfig(config, options.overwrite); // Create app config file 69 | generateDefaultFiles(config); 70 | 71 | if (options.createApp) createApp(config); 72 | else cli.log('App initiated', {success: true}); 73 | } 74 | 75 | function generateDefaultFiles (config, done) { 76 | var filesToCopy = {}; 77 | filesToCopy[config.error_page] = path.resolve(__dirname, '../templates/error.html'); 78 | filesToCopy['index.html'] = path.resolve(__dirname, '../templates/index.html'); 79 | 80 | copy({ 81 | files: filesToCopy, 82 | dest: path.resolve(cli.cwd.cwd, config.root), 83 | overwrite: false 84 | }, done); 85 | } 86 | 87 | function createApp (config) { 88 | cli.commands.create(config.name, function (err, body) { 89 | if (err) return cli.error(err); 90 | cli.log('App initiated', {success: true}); 91 | }, true); 92 | } 93 | }); 94 | 95 | function currentDirName () { 96 | return slug(path.basename(cli.cwd.cwd)); 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /lib/commands/login.js: -------------------------------------------------------------------------------- 1 | var promptly = require('promptly'); 2 | var request = require('request'); 3 | var url = require('url'); 4 | var format = require('chalk'); 5 | var openWithFallback = require('../helpers/open_with_fallback'); 6 | var read = require('read'); 7 | 8 | var POLL_TIMEOUT = 5000; // Poll api every 5 sedonds 9 | var INITIAL_POLL_TIMEOUT = 3000; 10 | var fallbackMessage = 'Please open this URL in your favorite browser to continue authentication:'; 11 | 12 | module.exports = function (cli) { 13 | var command = cli.command('login'); 14 | 15 | command.description('login to Divshot from the web'); 16 | command.handler(function (done) { 17 | var host = cli.api.options.host; 18 | 19 | cli.api.user.generateTicket().then(function (res) { 20 | 21 | if (!res.authorize_url) { 22 | return done(cli.errors.AUTH_TICKET_ERROR); 23 | } 24 | 25 | cli.log(); 26 | cli.log(format.yellow('=== Logging into Divshot ===')); 27 | 28 | openWithFallback(res.authorize_url, fallbackMessage + '\n' + res.authorize_url, function (code) { 29 | read({silent: true, prompt: ''}, cli.log.bind(cli)); 30 | 31 | process.stdout.write('\nWaiting for authorization ...'); 32 | 33 | setTimeout(function () { checkAuthStatus(res.ticket); }, INITIAL_POLL_TIMEOUT); 34 | }); 35 | }, cli.log.bind(cli)); 36 | 37 | function checkAuthStatus (ticket) { 38 | process.stdout.write('.'); 39 | 40 | cli.api.user.checkTicketStatus(ticket, function (err, res, body) { 41 | if (res.statusCode == 200) { 42 | var email = (body && body.user) ? body.user.email : 'a Divshot user'; 43 | 44 | cli.user.set('token', body.access_token); 45 | 46 | cli.log('\n'); 47 | cli.log('You have successfully authenticated as ' + format.bold(email), {success:true}); 48 | cli.log(); 49 | 50 | cli.log('For more information about how to use the command-line interface, try divshot help or visit ' + format.bold('http://docs.divshot.com/guides/getting-started')); 51 | 52 | done(null, body); 53 | process.exit(0); 54 | return; 55 | } 56 | 57 | if (res.statusCode == 404) done(cli.errors.INVALID_TICKET); 58 | if (res.statusCode == 202) return setTimeout(function () { checkAuthStatus(ticket); }, POLL_TIMEOUT); 59 | if (res.statusCode == 410) done(cli.errors.UNABLE_TO_AUTHENTICATE_TICKET); 60 | 61 | process.exit(1); 62 | }); 63 | } 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /lib/commands/logout.js: -------------------------------------------------------------------------------- 1 | module.exports = function (cli) { 2 | var command = cli.command('logout'); 3 | 4 | command.description('logout from Divshot'); 5 | command.handler(function (done) { 6 | cli.user.logout(); 7 | cli.log('Logged out', {success: true}); 8 | done(); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /lib/commands/open.js: -------------------------------------------------------------------------------- 1 | var open = require('open'); 2 | var format = require('chalk'); 3 | 4 | module.exports = function (cli) { 5 | cli.command('open ') 6 | .before('isApp') 7 | .description('open the current app in a browser') 8 | .handler(function (environment, done) { 9 | var config = cli.cwd.getConfig(); 10 | 11 | if (!cli.environments.isEnvironment(environment || cli.environments.default())) return done(cli.errors.INVALID_ENVIRONMENT); 12 | if (!config.name) return done(cli.errors.DIRECTORY_NOT_APP); 13 | 14 | var uri = config.name + '.divshot.io'; 15 | var fullUrl = (environment) 16 | ? 'http://' + environment + '.' + uri 17 | : 'http://' + uri; 18 | 19 | cli.log('Opening ' + format.bold(fullUrl)); 20 | 21 | open(fullUrl); 22 | }); 23 | }; -------------------------------------------------------------------------------- /lib/commands/performance.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | 3 | module.exports = function (cli) { 4 | var production = cli.command('performance', 'production') 5 | .description('upgrade the current app to performance status') 6 | .before('authenticate', 'isApp') 7 | .handler(function (done) { 8 | var config = cli.cwd.getConfig(); 9 | 10 | cli.api.apps.id(config.name).get(function (err, app) { 11 | cli.log(); 12 | 13 | if (app.production) cli.log(format.bold(config.name) + ' currently has performance status.'); 14 | else cli.log('Application ' + format.bold(config.name) + ' does not currently have performance status.'); 15 | 16 | done(null, app.production); 17 | }); 18 | }); 19 | 20 | production.task('on') 21 | .description('turn performance status on') 22 | .handler(function (done) { 23 | var config = cli.cwd.getConfig(); 24 | 25 | cli.api.apps.id(config.name).update({ 26 | production: true 27 | }, function (err, res) { 28 | cli.log(); 29 | 30 | if (err) return done(err); 31 | if (res.status === 401) return done(cli.errors.NOT_ADMIN); 32 | if (res.status === 402) return done(cli.errors.NO_PRODUCTION_SLOTS); 33 | 34 | cli.log('Application ' + format.bold(config.name) + ' has been upgraded to performance status. CDN provisioning may take up to 10 minutes.', {success: true}); 35 | done(); 36 | }); 37 | }); 38 | 39 | production.task('off') 40 | .description('turn performance status off') 41 | .handler(function (done) { 42 | var config = cli.cwd.getConfig(); 43 | 44 | cli.api.apps.id(config.name).update({ 45 | production: false 46 | }, function (err, res) { 47 | cli.log(); 48 | 49 | if (err) return done(err); 50 | if (res.status === 401) return done(cli.errors.NOT_ADMIN); 51 | if (res.status === 402) return done(cli.errors.NO_PRODUCTION_SLOTS); 52 | 53 | cli.log('Application ' + format.bold(config.name) + ' has been downgraded to non-performance status. CDN and custom domain SSL are now disabled for this app.', {success: true}); 54 | done(); 55 | }); 56 | }); 57 | }; -------------------------------------------------------------------------------- /lib/commands/promote.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | var environments = require('../environments'); 3 | 4 | module.exports = function (cli) { 5 | 6 | var command = cli.command('promote '); 7 | 8 | command.before('authenticate', 'isApp'); 9 | command.description('promote one environment to another'); 10 | 11 | command.handler(function (from, to, done) { 12 | 13 | var bold = format.bold; 14 | 15 | if (!from || !to) { 16 | return done(cli.errors.MISSING_PROMOTE_ENVIRONMENTS); 17 | } 18 | 19 | var name = cli.cwd.getConfig().name; 20 | 21 | cli.log(); 22 | cli.log('Promoting ' + bold(from) + ' to ' + bold(to) + ' ...'); 23 | cli.api.apps.id(name).releases.env(to).promote(from, function (err, release) { 24 | 25 | if (release.status == 404) { 26 | return done(cli.errors.INVALID_ENVIRONMENT); 27 | } 28 | 29 | if (err) { 30 | return done(cli.errors.DEFAULT); 31 | } 32 | 33 | cli.log(name + ' ' + bold(from) + ' promoted to ' + bold(to) , {success: true}); 34 | done(null, release); 35 | }); 36 | }); 37 | }; -------------------------------------------------------------------------------- /lib/commands/protect.js: -------------------------------------------------------------------------------- 1 | var regular = require('regular'); 2 | var format = require('chalk'); 3 | var _ = require('lodash'); 4 | 5 | module.exports = function (cli) { 6 | var command = cli.command('protect '); 7 | 8 | command.before('authenticate', 'isApp'); 9 | command.description('add http basic auth to any environment'); 10 | 11 | command.handler(function (env, credentials, done) { 12 | 13 | cli.log(); 14 | 15 | if (!env) { 16 | return done(cli.errors.MISSING_ENVIRONMENT + protectUsage()); 17 | } 18 | 19 | if (!credentials) { 20 | return done(cli.errors.MISSING_CREDENTIALS + protectUsage()); 21 | } 22 | 23 | var username = credentials.split(':')[0]; 24 | var password = credentials.split(':')[1]; 25 | 26 | if (!regular.basicAuth.test(credentials) || !username || !password) { 27 | return done(cli.errors.INVALID_CREDENTIALS); 28 | } 29 | 30 | cli.commands.apps(function (err, apps) { 31 | 32 | // TODO: refactor this out for reuse 33 | var name = cli.cwd.getConfig().name; 34 | var appObj; 35 | 36 | _.each(apps, function (orgApps) { 37 | var app = _.find(orgApps, function (app) { 38 | return app.name === name; 39 | }); 40 | 41 | if (app) appObj = app; 42 | }); 43 | 44 | if (!appObj) return done('The app ' + format.bold(name) + ' does not exist'); 45 | 46 | cli.api.apps.id(appObj.name).env(env).config({ 47 | auth: credentials 48 | }, function (err, response, body) { 49 | 50 | if (body && body.error) { 51 | return done(body.error) 52 | } 53 | 54 | // Assuming that environment is invalid with a 500 error 55 | if (response.statusCode == 500) { 56 | return done(cli.errors.INVALID_ENVIRONMENT); 57 | } 58 | 59 | if (response.statusCode == 400) { 60 | return done(cli.errors.BLANK_BUILD); 61 | } 62 | 63 | if (err || response.statusCode !== +200) { 64 | return done(cli.errors.DEFAULT); 65 | } 66 | 67 | cli.log(format.bold(env) + ' has been protected.', {success: true}); 68 | done(err, response); 69 | }); 70 | }); 71 | }); 72 | }; 73 | 74 | function protectUsage () { 75 | 76 | return '\n\n Example Usage:\n\n ' + format.bold('divshot protect [environment] [username:password]'); 77 | } -------------------------------------------------------------------------------- /lib/commands/pull.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var _ = require('lodash'); 3 | var join = require('join-path'); 4 | var request = require('request'); 5 | var writeStreamP = require('writestreamp'); 6 | var zlib = require('zlib'); 7 | var format = require('chalk'); 8 | var mime = require('mime-types'); 9 | 10 | /* 11 | divshot pull development 12 | events.js:72 throw er; // Unhandled 'error' event ^ Error: EISDIR, open '/Users/ant/Development/XVim/Web/images/' 13 | 14 | This is for a cleanly init'd local dovecot app. Please would you advise, thanks. 15 | */ 16 | 17 | module.exports = function (cli) { 18 | cli.command('pull ', 'download ') 19 | .before('authenticate') 20 | .description('download files from the given environment') 21 | .handler(function (environment, targetDir, done) { 22 | 23 | if (!environment) return done(cli.errors.MISSING_ENVIRONMENT); 24 | 25 | cli.log(); 26 | cli.log('Pulling current files from ' + environment); 27 | 28 | targetDir = targetDir || './'; 29 | 30 | cli.commands.files(environment, function (err, files) { 31 | 32 | if (err) return done(err); 33 | 34 | var appName = cli.cwd.getConfig().name; 35 | var appUrl = 'http://' + environment + '.' + appName + '.divshot.io'; 36 | 37 | cli.log(); 38 | 39 | var next = _.after(files.length, function () { 40 | 41 | cli.log(); 42 | cli.log('All files downloaded', {success: true}); 43 | done(); 44 | }); 45 | 46 | // TODO: 47 | // Right now, these files are just proxied from the live site. 48 | // Maybe we should proxy directly from remote store. 49 | // 50 | // Files for non-hashed builds are stored at / 51 | // Files for hashed builds are stoerd at / 52 | // 53 | 54 | _.each(files, function (filename) { 55 | 56 | // If this is true, this is probably a directory 57 | if (isDirectory(filename)) { 58 | return; 59 | } 60 | 61 | var writeFilename = join(process.cwd(), targetDir, filename); 62 | 63 | request.get(join(appUrl, filename)) 64 | .on('response', function (res) { 65 | 66 | var responder = res; 67 | 68 | if (res.headers['content-encoding'] === 'gzip') { 69 | responder = res.pipe(zlib.createGunzip()) 70 | } 71 | 72 | responder 73 | .pipe(writeStreamP(writeFilename, { 74 | encoding: 'buffer' 75 | })) 76 | .on('close', function () { 77 | 78 | cli.log('Downloaded ' + filename, {success: true}); 79 | next(); 80 | }) 81 | .on('error', function (err) { 82 | 83 | done(err); 84 | }); 85 | }) 86 | .on('error', function (err) { 87 | 88 | done(cli.errors.SERVER_NOT_AVAILABLE + ' with error: \n' + format.bold(err.message)); 89 | }); 90 | }); 91 | }); 92 | 93 | function isDirectory (pathanme) { 94 | 95 | return !mime.lookup(pathanme); 96 | } 97 | }); 98 | }; -------------------------------------------------------------------------------- /lib/commands/purge.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | 3 | module.exports = function (cli) { 4 | 5 | cli.command('purge ') 6 | .description('manually purge your app\'s cache') 7 | .before('authenticate', 'isApp') 8 | .handler(purgeHandler); 9 | 10 | function purgeHandler (environment, done) { 11 | 12 | // No environment 13 | if (arguments.length < 2) { 14 | done = environment; 15 | environment = undefined; 16 | } 17 | 18 | var appName = cli.cwd.getConfig().name; 19 | var app = cli.api.apps.id(appName); 20 | 21 | if (environment) { 22 | app = app.env(environment); 23 | } 24 | 25 | cli.log(); 26 | cli.log('Purging cache ...'); 27 | 28 | app.purgeCache(function (err, response) { 29 | 30 | if (err) { 31 | return done(err.message); 32 | } 33 | 34 | if (response.statusCode == 401) { 35 | return done(cli.errors.NOT_ADMIN); 36 | } 37 | 38 | if (response.statusCode >= 400) { 39 | return done(cli.errors.DEFAULT); 40 | } 41 | 42 | if (environment) { 43 | cli.log('You app\'s cache for ' + format.bold(environment) + ' has been purged.', {success: true}); 44 | } 45 | else { 46 | cli.log('Your app\'s cache has been purged.', {success: true}); 47 | } 48 | }); 49 | } 50 | }; -------------------------------------------------------------------------------- /lib/commands/push.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | var ProgressBar = require('progress'); 3 | var push = require('divshot-push'); 4 | 5 | var DIVSHOT_API_VERSION = '0.5.0'; 6 | var DIVSHOT_API_HOST = 'https://api.divshot.com'; 7 | 8 | module.exports = function (cli) { 9 | var command = cli.command('push ', 'deploy '); 10 | 11 | command.before('authenticate', 'isApp'); 12 | command.description('deploy your app to the specified environment'); 13 | command.handler(function (environment, done) { 14 | 15 | var progressBar; 16 | 17 | cli.log(''); 18 | 19 | if (environment === 'production') { 20 | cli.log(format.yellow('Note:') + ' Deploying to production purges your application\'s CDN cache, which may take up to one minute.\n'); 21 | } 22 | 23 | var pushStatus = push({ 24 | root: process.cwd(), 25 | environment: environment, 26 | config: cli.cwd.getConfig(), 27 | token: cli.api.options.token, 28 | timeout: cli.timeout, 29 | cacheDirectory: '.divshot-cache/deploy', 30 | hosting: { 31 | bucket: process.env.DIVSHOT_HASHED_BUCKET, 32 | api: { 33 | host: process.env.API_HOST || DIVSHOT_API_HOST, 34 | version: process.env.API_VERSION || DIVSHOT_API_VERSION 35 | } 36 | } 37 | }); 38 | 39 | pushStatus.onEnd(function (data) { 40 | 41 | process.stdout.write('\n'); 42 | process.stdout.write('Application deployed to ' + format.bold.white(data.environment) + '\n'); 43 | process.stdout.write('You can view your app at: ' + format.bold(data.url) + '\n'); 44 | 45 | done(null, data.url); 46 | }); 47 | 48 | pushStatus.onError(function (err) { 49 | 50 | if (err.statusCode == 500) { 51 | err = cli.errors.DEFAULT; 52 | } 53 | 54 | done(err); 55 | }); 56 | 57 | // Build status 58 | pushStatus 59 | .onBuild('start', function () { 60 | 61 | process.stdout.write('Creating build ... '); 62 | }) 63 | .onBuild('end', function (build) { 64 | 65 | process.stdout.write(format.green('✔') + '\n'); 66 | }); 67 | 68 | // Hashing status 69 | pushStatus 70 | .onHashing('start', function () { 71 | 72 | process.stdout.write('Hashing Directory Contents ...'); 73 | }) 74 | .onHashing('end', function () { 75 | 76 | process.stdout.write(format.green(' ✔') + '\n'); 77 | }); 78 | 79 | // Finalizing status 80 | pushStatus 81 | .onFinalize('start', function () { 82 | 83 | process.stdout.write('\nFinalizing build ... '); 84 | }) 85 | .onFinalize('end', function () { 86 | 87 | process.stdout.write(format.green('✔') + '\n'); 88 | }) 89 | 90 | // Release status 91 | pushStatus 92 | .onRelease('start', function (environment) { 93 | 94 | process.stdout.write('Releasing build to ' + format.bold(environment) + ' ... '); 95 | }) 96 | .onRelease('end', function () { 97 | 98 | process.stdout.write(format.green('✔') + '\n'); 99 | }); 100 | 101 | // App creation status (if needed) 102 | pushStatus 103 | .onApp('create', function (appName) { 104 | 105 | process.stdout.write(' App does not yet exist. Creating app ' + format.bold(appName) + ' ... '); 106 | }) 107 | 108 | // Upload status 109 | pushStatus 110 | .onUpload('start', function (fileCount) { 111 | 112 | cli.log(); 113 | 114 | progressBar = new ProgressBar('Syncing '+ fileCount +' files: [' + format.green(':bar') + '] ' + format.bold(':percent') + '', { 115 | complete: '=', 116 | incomplete: ' ', 117 | width: 50, 118 | total: fileCount 119 | }); 120 | }) 121 | .onUpload('progress', function (count) { 122 | 123 | progressBar.tick(count); 124 | }) 125 | .onUpload('retry', function (err) { 126 | 127 | console.log('\n' + format.red.underline(err.message)); 128 | console.log(format.green.underline('Retrying...')); 129 | }); 130 | }); 131 | }; 132 | -------------------------------------------------------------------------------- /lib/commands/rename.js: -------------------------------------------------------------------------------- 1 | var promptly = require('promptly'); 2 | var format = require('chalk'); 3 | 4 | module.exports = function (cli) { 5 | var command = cli.command('rename '); 6 | 7 | command.before('authenticate', 'isApp'); 8 | command.description('change the name of the app'); 9 | command.handler(function (name, done) { 10 | var config = cli.cwd.getConfig(); 11 | var originalName = config.name; 12 | 13 | if (!name) return done(cli.errors.MISSING_APP_NAME); 14 | 15 | promptly.confirm('Are you sure you want to rename this app? It will be permanant and change the app\'s url. (y/n)', function (err, rename) { 16 | if (!rename) return done(); 17 | 18 | cli.log('Renaming app to ' + format.bold(name) + ' ...'); 19 | 20 | cli.api.apps.id(originalName).update({name: name}, function (err, response) { 21 | if (response.error) return done(response.error); 22 | if (response.statusCode && response.statusCode >= 400) return done(cli.errors.DEFAULT); 23 | 24 | cli.cwd.setConfigValue('name', name); 25 | cli.log(format.bold(originalName) + ' has been renamed to ' + format.bold(name), {success: true}); 26 | done(null, config); 27 | }); 28 | }); 29 | }); 30 | }; -------------------------------------------------------------------------------- /lib/commands/request.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var format = require('chalk'); 3 | var print = require('pretty-print'); 4 | var request = require('request'); 5 | var join = require('join-path'); 6 | 7 | var HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch']; 8 | 9 | module.exports = function (cli) { 10 | 11 | var command = cli.command('request') 12 | .secret(true) 13 | .handler(function () { 14 | 15 | cli.log(); 16 | cli.log('Please use ' + format.bgYellow.black(' divshot request: ') + ' if you want to make a request.'); 17 | cli.log(); 18 | cli.log('Available http methods:'); 19 | cli.log(); 20 | print(HTTP_METHODS, { 21 | leftPadding: 2 22 | }); 23 | }); 24 | 25 | HTTP_METHODS.forEach(function (method) { 26 | 27 | command.task(method).handler(methodHandler); 28 | 29 | function methodHandler (url, body, done) { 30 | 31 | // Ensure callback and body values 32 | if (typeof body === 'function') { 33 | done = body; 34 | body = undefined; 35 | } 36 | 37 | request[method.toLowerCase()]({ 38 | url: join(cli.api.options.host, url), 39 | headers: { 40 | Authorization: 'Bearer ' + cli.user.get('token') 41 | }, 42 | body: body 43 | }, function (err, response, body) { 44 | 45 | if (err) { 46 | return done(err); 47 | } 48 | 49 | // Convert to object so we can format 50 | if (_.isString(body)) { 51 | body = JSON.parse(body); 52 | } 53 | 54 | // Format 55 | if (_.isObject(body)) { 56 | body = JSON.stringify(body, null, 2); 57 | } 58 | 59 | cli.log(); 60 | cli.log(body); 61 | cli.log(); 62 | }); 63 | } 64 | }); 65 | }; -------------------------------------------------------------------------------- /lib/commands/rollback.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var format = require('chalk'); 3 | 4 | module.exports = function (cli) { 5 | 6 | var command = cli.command('rollback '); 7 | 8 | command.before('authenticate', 'isApp'); 9 | command.description('rollback an environment to a previous release'); 10 | command.handler(function (environment, done) { 11 | 12 | if (!environment) { 13 | return done(cli.errors.MISSING_ENVIRONMENT); 14 | } 15 | 16 | var config = cli.cwd.getConfig(); 17 | var releaseVersion = _.last(cli.args.args); 18 | var app = cli.api.apps.id(config.name).releases.env(environment); 19 | 20 | cli.log(); 21 | 22 | // Rollback to specific version 23 | if (cli.args.args.length > 1 && _.isNumber(releaseVersion)) { 24 | cli.log('Rolling back ' + format.bold(environment) + ' to version ' + releaseVersion + ' ...'); 25 | app.rollbackTo(releaseVersion, responseHandler); 26 | } 27 | 28 | // Rollback to previous version 29 | else { 30 | cli.log('Rolling back ' + format.bold(environment) + ' to previous release ...'); 31 | app.rollback(responseHandler); 32 | } 33 | 34 | function responseHandler (err, response) { 35 | 36 | if (err) { 37 | return done(cli.errors.DEFAULT); 38 | } 39 | 40 | if (response.statusCode == 403) { 41 | return done(cli.errors.INVALID_RELEASE_VERSION); 42 | } 43 | 44 | cli.log(format.bold(environment) + ' has been rolled back for ' + format.bold(config.name) + '.', {success: true}); 45 | done(); 46 | } 47 | }); 48 | }; -------------------------------------------------------------------------------- /lib/commands/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var spawn = require('child_process').spawn; 3 | 4 | module.exports = function (cli) { 5 | var command = cli.command('server ', 's '); 6 | 7 | command.description('start server for local development'); 8 | command.handler(function (customDir, done) { 9 | 10 | var dirname = __dirname; 11 | 12 | // Avoid spaces in dir path on Windows 13 | if (isWindows()) { 14 | dirname = dirname.replace(' ', '^ '); 15 | } 16 | 17 | var cmd = path.resolve(dirname, '../../node_modules/.bin/superstatic'); 18 | var server; 19 | 20 | // On Windows, spawn has to delegate its work to cmd to avoid ENOENT error. 21 | if (isWindows()) { 22 | server = spawn('cmd', ['/s','/c', cmd, process.argv.splice(3)], { 23 | windowsVerbatimArguments: true 24 | }); 25 | } 26 | else { 27 | server = spawn(cmd , process.argv.splice(3)); 28 | } 29 | 30 | server.stdout.pipe(process.stdout); 31 | 32 | server.stderr.on('data', function (data) { 33 | done(cli.errors.SERVER_ERROR); 34 | }); 35 | 36 | server.on('close', function () { 37 | done(); 38 | }); 39 | 40 | server.on('error', function (err) { 41 | 42 | console.error(err.message); 43 | console.error(err.stack); 44 | }); 45 | }); 46 | }; 47 | 48 | function isWindows () { 49 | return process && process.env && process.env.OS && process.env.OS.toLowerCase().indexOf('windows') >= 0; 50 | } -------------------------------------------------------------------------------- /lib/commands/stats.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var format = require('chalk'); 3 | var bytes = require('bytes'); 4 | var pluralize = require('pluralize'); 5 | 6 | module.exports = function (cli) { 7 | 8 | cli.command('stats') 9 | .description('Show various stats for your app') 10 | .before('authenticate', 'isApp') 11 | .handler(function (done) { 12 | 13 | var appName = cli.cwd.getConfig().name; 14 | 15 | cli.api.apps.id(appName).stats(function (err, stats) { 16 | 17 | var stats = _(stats) 18 | .map(function (value, key) { 19 | 20 | var title = caseWords(key); 21 | var description = parseDescription(value); 22 | 23 | return [title, description]; 24 | }) 25 | .zipObject() 26 | .value(); 27 | 28 | cli.log(); 29 | cli.log(' Stats for ' + format.bold(appName)); 30 | cli.log(); 31 | cli.logObject(stats); 32 | 33 | done(); 34 | }); 35 | }); 36 | }; 37 | 38 | function parseDescription (data) { 39 | // 0.65 GB (800 requests) 40 | 41 | return bytes(data.bandwidth) + ' (' + abbreviateNumber(data.requests) + ' ' + pluralize('requests', data.requests) + ')'; 42 | } 43 | 44 | function caseWords (sentence) { 45 | return sentence.split('_').map(function (word) { 46 | return ucfirst(word); 47 | }).join(' '); 48 | } 49 | 50 | function ucfirst (s) { 51 | return s.substr(0, 1).toUpperCase() + s.substring(1); 52 | }; 53 | 54 | function abbreviateNumber(value) { 55 | var newValue = value; 56 | if (value >= 1000) { 57 | var suffixes = ["", "k", "m", "b","t"]; 58 | var suffixNum = Math.floor( (""+value).length/3 ); 59 | var shortValue = ''; 60 | for (var precision = 2; precision >= 1; precision--) { 61 | shortValue = parseFloat( (suffixNum != 0 ? (value / Math.pow(1000,suffixNum) ) : value).toPrecision(precision)); 62 | var dotLessShortValue = (shortValue + '').replace(/[^a-zA-Z 0-9]+/g,''); 63 | if (dotLessShortValue.length <= 2) { break; } 64 | } 65 | if (shortValue % 1 != 0) shortNum = shortValue.toFixed(1); 66 | newValue = shortValue+suffixes[suffixNum]; 67 | } 68 | return newValue; 69 | } -------------------------------------------------------------------------------- /lib/commands/status.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var format = require('chalk'); 3 | var moment = require('moment'); 4 | var async = require('async'); 5 | 6 | module.exports = function (cli) { 7 | var command = cli.command('status '); 8 | 9 | command.before('authenticate', 'isApp'); 10 | command.description('show release info for each environment'); 11 | command.handler(function (environment, done) { 12 | var appName = cli.cwd.getConfig().name; 13 | 14 | // Single 15 | if (environment) return getEnvironmentReleases(appName, environment); 16 | 17 | // All 18 | async.eachSeries(cli.environments, function (env, next) { 19 | getIndividualReleaseInfo(appName, env, next); 20 | }, function (err, releases) { 21 | if (err) return done(cli.errors.DEFAULT); 22 | done(null, releases) 23 | }); 24 | 25 | function getIndividualReleaseInfo(appName, environment, callback) { 26 | callback = callback || function () {}; 27 | 28 | getReleases(appName, environment, function (err, releases) { 29 | var release = _.last(releases); 30 | 31 | if (!release || !release.build) { 32 | cli.log(); 33 | cli.log('You haven\'t released anything to ' + format.bold(environment) + ' for ' + format.bold(appName)); 34 | return callback(); 35 | } 36 | 37 | cli.log(); 38 | cli.log('Latest ' + format.bold(environment) + ' release for ' + format.bold(appName)); 39 | printRelease(release, appName, environment); 40 | callback(err, release); 41 | }); 42 | } 43 | 44 | function getEnvironmentReleases (appName, environment) { 45 | getReleases(appName, environment, function (err, releases) { 46 | cli.log(); 47 | 48 | if (err) return done(err); 49 | if (releases[0] == 403) return done(releases[1]); 50 | if (releases[0] === 404) return done(releases[1]); 51 | 52 | cli.log(); 53 | cli.log(format.bold(environment) + ' releases for ' + format.bold(appName)); 54 | 55 | _.each(releases, function (release) { 56 | printRelease(release, appName, environment); 57 | }); 58 | 59 | done(null, releases); 60 | }); 61 | } 62 | 63 | function getReleases (appName, environment, callback) { 64 | cli.api.apps.id(appName).releases.env(environment).get(function (err, releases) { 65 | callback(err, _.sortBy(releases, function (release) { return release.version; })); 66 | }); 67 | } 68 | 69 | function printRelease (release, appName, environment) { 70 | var printObj = { 71 | 'release #': release.version, 72 | 'build id': release.build.id, 73 | 'released by': release.author, 74 | 'released on': moment.unix(release.timestamp).format('dddd, MMMM Do YYYY, h:mm:ss a') 75 | }; 76 | 77 | cli.log(); 78 | cli.logObject(printObj, {rightPadding: 3}); 79 | } 80 | }); 81 | }; -------------------------------------------------------------------------------- /lib/commands/unprotect.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | var _ = require('lodash'); 3 | 4 | module.exports = function (cli) { 5 | var command = cli.command('unprotect '); 6 | 7 | command.before('authenticate', 'isApp'); 8 | command.description('remove http basic auth to any environment'); 9 | command.handler(function (env, done) { 10 | var appName = cli.cwd.getConfig().name; 11 | 12 | if (!env) return done(cli.errors.MISSING_ENVIRONMENT); 13 | 14 | cli.commands.apps(function (err, apps) { 15 | var appObj; 16 | 17 | _.each(apps, function (orgApps) { 18 | var app = _.find(orgApps, function (app) { 19 | return app.name === appName; 20 | }); 21 | 22 | if (app) appObj = app; 23 | }); 24 | 25 | if (!appObj) return done(format.bold(appName) + ' app does not exist'); 26 | 27 | cli.api.apps.id(appObj.id).env(env).config({ 28 | auth: '' 29 | }, function (err, response) { 30 | if (err || response.statusCode !== +200) return done(cli.errors.DEFAULT); 31 | 32 | cli.log(format.bold(env) + ' is now unprotected for ' + format.bold(appName) + '.', {success: true}); 33 | done(); 34 | }); 35 | }); 36 | }); 37 | }; -------------------------------------------------------------------------------- /lib/cwd.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var _ = require('lodash'); 4 | var feedback = require('feedback'); 5 | var jsonlint = require('jsonlint'); 6 | 7 | var Cwd = function () { 8 | this.configFilename = 'divshot.json'; 9 | this.cwd = process.cwd(); 10 | this.filePath = path.join(process.cwd(), this.configFilename); 11 | this._overrideAppName = null; 12 | }; 13 | 14 | Cwd.prototype.getConfig = function () { 15 | var config = {}; 16 | 17 | if (this.hasConfig()) { 18 | var fileContents = fs.readFileSync(this.filePath).toString(); 19 | validateConfigFile(fileContents); 20 | config = JSON.parse(fileContents); 21 | } 22 | 23 | if (this._overrideAppName) { 24 | config.name = this._overrideAppName; 25 | } 26 | 27 | return config; 28 | }; 29 | 30 | Cwd.prototype.setConfig = function (data, overwrite) { 31 | if (!this.hasConfig()) fs.writeFileSync(this.filePath, stringify({})); 32 | 33 | var originalConfigFile = JSON.parse(fs.readFileSync(this.filePath)); 34 | var config = (overwrite) ? data : _.extend(originalConfigFile, data); 35 | 36 | fs.writeFileSync(this.filePath, stringify(config)); 37 | }; 38 | 39 | Cwd.prototype.setConfigValue = function (key, value) { 40 | var config = this.getConfig(); 41 | config[key] = value; 42 | this.setConfig(config, true); 43 | }; 44 | 45 | Cwd.prototype.removeConfigValue = function (key) { 46 | var config = this.getConfig(); 47 | delete config[key]; 48 | this.setConfig(config, true); 49 | }; 50 | 51 | Cwd.prototype.hasConfig = function () { 52 | return fs.existsSync(this.filePath); 53 | }; 54 | 55 | Cwd.prototype.stringify = function (data) { 56 | return JSON.stringify(data, null, 2); 57 | }; 58 | 59 | Cwd.prototype.overrideAppName = function (appName) { 60 | this._overrideAppName = appName; 61 | }; 62 | 63 | // Setters 64 | Cwd.prototype.setCwd = function (dir) { 65 | this.cwd = path.resolve(process.cwd(), dir); 66 | this.setConfigFilename(this.configFilename); 67 | }; 68 | 69 | Cwd.prototype.setConfigFilename = function (filename) { 70 | this.configFilename = filename; 71 | this.filePath = path.join(this.cwd, this.configFilename); 72 | }; 73 | 74 | module.exports = Cwd; 75 | 76 | function validateConfigFile (fileContents) { 77 | try { 78 | jsonlint.parse(fileContents); 79 | } 80 | catch (e) { 81 | var msgArr = e.message.split('\n'); 82 | var msg = msgArr[1] + '\n' + msgArr[2]; 83 | 84 | feedback.info(); 85 | feedback.error('Invalid Divshot configuration file. ' + msgArr[0] + '\n'); 86 | feedback.info(msg); 87 | 88 | process.exit(1); 89 | } 90 | } 91 | 92 | function stringify (data) { 93 | return JSON.stringify(data, null, 2); 94 | }; -------------------------------------------------------------------------------- /lib/environments.js: -------------------------------------------------------------------------------- 1 | var environments = ['development', 'staging', 'production']; 2 | 3 | environments.isEnvironment = function (env) { 4 | return environments.indexOf(env) > -1; 5 | }; 6 | 7 | environments.default = function () { 8 | return 'production'; 9 | }; 10 | 11 | module.exports = environments; 12 | 13 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | 3 | module.exports = { 4 | // misc 5 | DEFAULT: 'There was an unexpected error. Please try again. If the problem persists, please contact support.', 6 | NOT_AUTHENTICATED: 'You must log in with ' + format.bold('"divshot login"') + ' before you can do that.', 7 | DOMAIN_IN_USE: 'That domain is already in use.', 8 | FILES_NOT_RELEASED: 'Not all files released.', 9 | DIRECTORY_NOT_APP: 'The current directory is not an app. Please use ' + format.bold('divshot init') + ' to make this directory an app.', 10 | UNABLE_TO_AUTHENTICATE_TICKET: 'There was a problem authenticating. Please try again or contact support@divshot.com.', 11 | VOUCHER_USED: 'Unable to redeem voucher. It may have already been claimed.', 12 | VOUCHER_USED_BY_YOU: 'This voucher has already been applied to your account.', 13 | SERVER_ERROR: 'There was an unexpected error with the server. Perhaps the port is already in use.', 14 | EMAIL_TAKEN: 'That email is already in use.', 15 | NOT_ADMIN: 'You do not have access to perform this action on this app.', 16 | SERVER_NOT_AVAILABLE: 'There was an error reaching our servers. Please contact support.', 17 | BLANK_BUILD: 'You must deploy to this environment before you can execute this command.', 18 | 19 | // auth 20 | AUTH_TICKET_ERROR: 'There was an error generating an authorization session. Please try again. If this problem persists, please contact support.', 21 | 22 | // missing 23 | MISSING_CONFIG_KEY: 'Config key name is required.', 24 | MISSING_CONFIG_VALUE: 'Config value for key is required.', 25 | MISSING_PROMOTE_ENVIRONMENTS: 'You must provide the environment to promote and its destination environment.', 26 | MISSING_ENVIRONMENT: 'Application environment is required.', 27 | MISSING_CREDENTIALS: 'Credentials are required.', 28 | MISSING_APP_NAME: 'You must provide an app name.', 29 | MISSING_EMAIL: 'You must provide an email.', 30 | 31 | // invalid 32 | INVALID_TOKEN: 'Your session has expired or you have not logged in yet.', 33 | INVALID_DOMAIN: 'Invalid domain name. A valid domain looks like ' + format.bold('www.divshot.com') + '.', 34 | INVALID_CREDENTIALS: 'Invalid credentials.', 35 | INVALID_APP_NAME: 'Invalid app name.', 36 | INVALID_ENVIRONMENT: 'Invalid environment provided.', 37 | INVALID_TICKET: 'The authentication ticket is expired or invalid. Please try again.', 38 | INVALID_VOUCHER: 'Invalid voucher. Please provide a valid voucher code.', 39 | INVALID_EMAIL: 'Invalid email.', 40 | INVALID_URL: 'Please provide a valid url.', 41 | INVALID_HOOK_ID: 'That hook either does not exist or there was an error contacting our servers. Please try again or contact support.', 42 | INVALID_CERT: ' Invalid certificate or key. Please see ' + format.underline('http://docs.divshot.com/guides/ssl') + ' for help with certificates.', 43 | INVALID_RELEASE_VERSION: 'App release version does not exist.', 44 | 45 | // Upgrade 46 | UPGRADE_TO_USE_FEATURE: 'You must upgrade to a paid plan in order to use this feature.', 47 | UPGRADE_FOR_NON_PROD_DOMAINS: 'You must upgrade to a paid plan in order to use custom domains on non-production environments.', 48 | NO_PRODUCTION_SLOTS: 'You do not have any production slots available. Please downgrade an existing production app or visit ' + format.bold('https://dashboard.divshot.com') + ' to upgrade your account.', 49 | APP_NOT_PRODUCTION: 'This app is currently not a production app. SSL certificates require your app to be in production mode. Please see ' + format.underline('http://docs.divshot.com/guides/ssl') + ' for more information.', 50 | 51 | // Does not exists 52 | DIRECTORY_DOES_NOT_EXIST: 'Your app\'s root directory does not exist.', 53 | MUST_HAVE_DEPLOYED_APP: 'Please deploy at least one version of your app before doing this.' 54 | }; 55 | -------------------------------------------------------------------------------- /lib/helpers/bundle_files.js: -------------------------------------------------------------------------------- 1 | // var tarzan = require('tarzan'); 2 | // var through = require('through'); 3 | // var _ = require('lodash'); 4 | // var checkFileSizes = require('./check_files_sizes'); 5 | // var ignoreGlobs = [ 6 | // '.**/**', 7 | // '.**' 8 | // ]; 9 | 10 | // module.exports = function (appRootDir, exclude) { 11 | // var stream = through(); 12 | 13 | // checkFileSizes(appRootDir, function (err) { 14 | // if (err) return stream.emit('error', err); 15 | 16 | // var packOptions = { 17 | // directory: appRootDir, 18 | // ignore: _.union(ignoreGlobs, exclude) 19 | // }; 20 | 21 | // tarzan(packOptions) 22 | // .on('file', function (pathname) { 23 | // stream.emit('file', pathname); 24 | // }) 25 | // .pipe(stream); 26 | // }); 27 | 28 | // return stream; 29 | // }; 30 | -------------------------------------------------------------------------------- /lib/helpers/check_files_sizes.js: -------------------------------------------------------------------------------- 1 | var sizer = require('sizer'); 2 | var TEN_MB = 10000000; 3 | var logger = require('feedback'); 4 | var format = require('chalk'); 5 | var ignoreGlobs = [ 6 | '.**/**', 7 | '.**' 8 | ]; 9 | 10 | module.exports = function (directory, done) { 11 | sizer.bigger(TEN_MB, directory, ignoreGlobs, function (err, files) { 12 | if (files.length) { 13 | logger.error(format.red('Files MUST NOT be greater than 10MB.')); 14 | 15 | files.forEach(function (file) { 16 | logger.error(format.yellow(file) + ' is too big.'); 17 | }); 18 | } 19 | 20 | done(files.length); 21 | }); 22 | }; -------------------------------------------------------------------------------- /lib/helpers/command.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (app) { 4 | return function (name) { 5 | return _.find(app.program.commands, function (command) { 6 | return command._name === name; 7 | }); 8 | }; 9 | }; -------------------------------------------------------------------------------- /lib/helpers/format_user_emails.js: -------------------------------------------------------------------------------- 1 | var format = require('chalk'); 2 | 3 | module.exports = function (emails) { 4 | return emails.map(function (email) { 5 | var address = email.address; 6 | 7 | if (!email.confirmed) address += ' (unconfirmed)'; 8 | if (email.primary) address += ' (primary)'; 9 | 10 | return address; 11 | }); 12 | }; -------------------------------------------------------------------------------- /lib/helpers/loading.js: -------------------------------------------------------------------------------- 1 | var feedback = require('feedback'); 2 | 3 | module.exports = function (timing) { 4 | var interval; 5 | var callback = function () { 6 | clearInterval(interval); 7 | }; 8 | 9 | timing = timing || 500; 10 | 11 | interval = setInterval(function () { 12 | feedback.write('.'); 13 | }, timing); 14 | 15 | return callback; 16 | }; -------------------------------------------------------------------------------- /lib/helpers/open_with_fallback.js: -------------------------------------------------------------------------------- 1 | var open = require('open'); 2 | var format = require('chalk'); 3 | 4 | module.exports = function (url, fallbackMessage, callback, log) { 5 | log = log || console.log.bind(console); 6 | var p = open(url); 7 | 8 | p.on('exit', function(code) { 9 | if (parseInt(code) > 0) { 10 | log(); 11 | log(fallbackMessage) 12 | } 13 | else { 14 | log(); 15 | log('Opening ' + format.bold(url)); 16 | } 17 | 18 | callback(code); 19 | }); 20 | }; -------------------------------------------------------------------------------- /lib/logo.txt: -------------------------------------------------------------------------------- 1 | _____ _______ _______ _ _ ____ _______ 2 | | __ \_ _\ \ / / ____| | | |/ __ \__ __| 3 | | | | || | \ \ / / (___ | |__| | | | | | | 4 | | | | || | \ \/ / \___ \| __ | | | | | | 5 | | |__| || |_ \ / ____) | | | | |__| | | | 6 | |_____/_____| \/ |_____/|_| |_|\____/ |_| -------------------------------------------------------------------------------- /lib/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found 6 | 7 | 8 | 83 | 84 | 85 |
86 |
87 |

404 Not Found

88 |

Sorry, we were unable to locate anything at this address.

89 |

Should there be something here?

90 |

Get Help via Email or Common Deploy Issues Documentation

91 | 92 | 93 |
94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /lib/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Divshot Application 7 | 8 | 14 | 15 | 16 | 17 |

Welcome to Divshot Hosting!

18 |

Make something static!

19 |

Should there be something here?

20 |

Get Help via Email or Common Deploy Issues Documentation

21 | 22 | 23 | -------------------------------------------------------------------------------- /lib/user.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var mkdirp = require('mkdirp'); 4 | 5 | var User = function (baseDir) { 6 | this.baseDir = baseDir; 7 | this.attributes = {}; 8 | 9 | // Create user and config directories 10 | var configDir = path.join(this.baseDir, 'config'); 11 | mkdirp.sync(configDir); 12 | 13 | this.filePath = path.join(configDir, 'user.json'); 14 | 15 | this.load(); 16 | }; 17 | 18 | User.prototype.load = function () { 19 | if (!fs.existsSync(this.filePath)) { 20 | fs.writeFileSync(this.filePath, JSON.stringify({}, null, 2)); 21 | } 22 | 23 | var config = require(this.filePath); 24 | this.attributes = config; 25 | return config; 26 | }; 27 | 28 | User.prototype.save = function () { 29 | fs.writeFileSync(this.filePath, JSON.stringify(this.attributes, null, 2)); 30 | }; 31 | 32 | User.prototype.get = function (key) { 33 | return this.attributes[key]; 34 | }; 35 | 36 | User.prototype.set = function (key, value, save) { 37 | this.attributes[key] = value; 38 | if (save !== false) this.save(); 39 | }; 40 | 41 | User.prototype.logout = function () { 42 | delete this.attributes.token; 43 | this.save(); 44 | }; 45 | 46 | User.prototype.authenticated = function () { 47 | return this.attributes.token; 48 | }; 49 | 50 | module.exports = User; -------------------------------------------------------------------------------- /lib/validators/slug.js: -------------------------------------------------------------------------------- 1 | var regular = require('regular'); 2 | var feedback = require('feedback'); 3 | 4 | module.exports = function (value) { 5 | if (!regular.slug.test(value.toLowerCase())) { 6 | feedback.error('Name must be a valid subdomain value.'); 7 | throw ''; 8 | } 9 | 10 | return value; 11 | }; -------------------------------------------------------------------------------- /lib/validators/yes_no.js: -------------------------------------------------------------------------------- 1 | var feedback = require('feedback'); 2 | 3 | module.exports = function (value) { 4 | if (value === false || value === true) return value; 5 | 6 | var validValue = false; 7 | value = value.toLowerCase(); 8 | 9 | if (value === 'y' || value === 'yes') value = true; validValue = true; 10 | if (value === 'n' || value === 'no') value = false; validValue = true; 11 | 12 | if (!validValue) { 13 | feedback.error('You must answer either "y" or "n"'); 14 | throw ''; 15 | } 16 | 17 | return value; 18 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "divshot-cli", 3 | "description": "CLI for Divshot", 4 | "version": "1.10.5", 5 | "dependencies": { 6 | "async": "^0.9.0", 7 | "booly": "^1.0.2", 8 | "bytes": "^2.0.0", 9 | "chalk": "^1.0.0", 10 | "clone": "^1.0.0", 11 | "copy-files": "^0.1.0", 12 | "cozy-slug": "0.2.2", 13 | "divshot-api": "1.3.x", 14 | "divshot-dumper": "1.1.0", 15 | "divshot-push": "1.4.2", 16 | "exit-hook": "^1.1.1", 17 | "feedback": "^0.3.2", 18 | "fs-extra": "^0.18.0", 19 | "home-dir": "^1.0.0", 20 | "inquirer": "^0.8.0", 21 | "join-path": "^1.0.0", 22 | "jsonlint": "^1.6.2", 23 | "lodash": "^2.4.1", 24 | "marked": "^0.3.2", 25 | "marked-terminal": "^1.1.1", 26 | "mime-types": "^2.0.2", 27 | "mkdirp": "0.5.x", 28 | "moment": "^2.8.3", 29 | "nash": "0.3.x", 30 | "open": "0.0.5", 31 | "pluralize": "^1.0.3", 32 | "pretty-print": "^1.0.0", 33 | "progress": "^1.1.8", 34 | "promptly": "^0.2.0", 35 | "read": "^1.0.5", 36 | "regular": "^0.1.6", 37 | "request": "^2.44.0", 38 | "sizer": "^0.1.1", 39 | "slash": "^1.0.0", 40 | "superstatic": "1.2.1", 41 | "touch": "0.0.3", 42 | "update-notifier": "^0.2.2", 43 | "writestreamp": "^0.1.0" 44 | }, 45 | "devDependencies": { 46 | "jshint": "~2.6.0" 47 | }, 48 | "scripts": { 49 | "test": "node_modules/.bin/mocha --recursive --reporter spec ./test" 50 | }, 51 | "author": "Divshot", 52 | "homepage": "http://divshot.com", 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/divshot/divshot-cli.git" 56 | }, 57 | "bugs": { 58 | "url": "https://github.com/divshot/divshot-cli/issues" 59 | }, 60 | "keywords": [ 61 | "divshot", 62 | "hosting", 63 | "static" 64 | ], 65 | "preferGlobal": "true", 66 | "bin": { 67 | "divshot": "bin/divshot.js" 68 | }, 69 | "main": "bin/divshot.js" 70 | } 71 | --------------------------------------------------------------------------------