CopyShell.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. <?php
  2. App::uses('Folder', 'Utility');
  3. App::uses('File', 'Utility');
  4. App::uses('AppShell', 'Console/Command');
  5. /** Valid characters: letters,numbers,underscores,hyphens only */
  6. if (!defined('VALID_ALPHANUMERIC_HYPHEN_UNDERSCORE')) {
  7. define('VALID_ALPHANUMERIC_HYPHEN_UNDERSCORE', '/^[\da-zA-Z_-]+$/');
  8. }
  9. if (!defined('NL')) {
  10. define('NL', PHP_EOL);
  11. }
  12. if (!defined('WINDOWS')) {
  13. define('WINDOWS', substr(PHP_OS, 0, 3) === 'WIN' ? true : false);
  14. }
  15. /**
  16. * A Copy Shell to update changes to your life site.
  17. * It can also be used for backups and other "copy changes only" services.
  18. *
  19. * Using a PPTP tunnel it is also highly secure while being as fast as ftp tools
  20. * like scp or rsnyc or sitecopy can possibly be.
  21. *
  22. * tested on Console:
  23. * - Windows (XP, Vista, Win7, Win8)
  24. *
  25. * tested with:
  26. * - FTP update (updating a remote server)
  27. *
  28. * does not work with:
  29. * - SFTP (although supported in newest version there are errors)
  30. *
  31. * based on:
  32. * - sitecopy [linux] (maybe switch to "rsync" some time...? way more powerful and reliable!)
  33. *
  34. * @author Mark Scherer
  35. * @license MIT
  36. * @cakephp 2.x
  37. */
  38. class CopyShell extends AppShell {
  39. public $scriptFolder = null;
  40. public $sitecopyFolder = null;
  41. public $sitecopyFolderName = 'copy';
  42. public $configCustomFile = 'sitecopy.txt'; // inside /config
  43. public $configGlobalFile = 'sitecopy_global.txt'; // inside /config
  44. public $configCount = 0; # total count of all configs (all types)
  45. const TYPE_APP = 0;
  46. const TYPE_CAKE = 1;
  47. const TYPE_PLUGIN = 2;
  48. const TYPE_VENDOR = 3;
  49. const TYPE_CUSTOM = 4;
  50. public $types = array(
  51. self::TYPE_APP => 'app',
  52. self::TYPE_CAKE => 'cake',
  53. self::TYPE_VENDOR => 'vendor',
  54. self::TYPE_PLUGIN => 'plugin',
  55. self::TYPE_CUSTOM => 'custom'
  56. );
  57. public $matches = array(
  58. self::TYPE_CAKE => 'lib/Cake',
  59. self::TYPE_VENDOR => 'vendors', # in root dir
  60. self::TYPE_PLUGIN => 'plugins', # in root dir
  61. );
  62. public $type = self::TYPE_APP;
  63. public $configName = null; # like "test" in "app_test" or "123" in "custom_123"
  64. # both typeName and configName form the "site" name: typeName_configName
  65. public $configCustom = array(); # configFile Content
  66. public $configGlobal = array(); # configFile Content
  67. public $tmpFolder = null;
  68. public $tmpFile = null;
  69. public $logFile = null;
  70. public $localFolder = APP;
  71. public $remoteFolder = null;
  72. /**
  73. * CopyShell::startup()
  74. *
  75. * @return void
  76. */
  77. public function startup() {
  78. $this->scriptFolder = dirname(__FILE__) . DS;
  79. $this->sitecopyFolder = $this->scriptFolder . $this->sitecopyFolderName . DS;
  80. $this->tmpFolder = TMP . 'cache' . DS . 'copy' . DS;
  81. /*
  82. TODO - garbige clean up of log file
  83. if (file_exists($this->tmpFolder.'log.txt') && (int)round(filesize($this->tmpFolder.'log.txt')/1024) > 2000) { # > 2 MB
  84. unlink($this->tmpFolder.'log.txt');
  85. }
  86. //echo (int)round(filesize($this->tmpFolder.'log.txt')/1024);
  87. */
  88. parent::startup();
  89. }
  90. /**
  91. * Main method to run updates
  92. * to use params you need to explicitly call "Tools.Copy main -params"
  93. *
  94. * @return void
  95. */
  96. public function run() {
  97. $configContent = $this->getConfigs();
  98. # change type if param given
  99. if (!empty($this->params['cake'])) { # cake core
  100. $this->type = self::TYPE_CAKE;
  101. } elseif (!empty($this->params['vendors'])) {
  102. $this->type = self::TYPE_VENDOR;
  103. } elseif (!empty($this->params['plugins'])) {
  104. $this->type = self::TYPE_PLUGIN;
  105. }
  106. $this->out($this->types[$this->type]);
  107. # find all mathing configs to this type
  108. $configs = array();
  109. if (!empty($configContent)) {
  110. $configs = $this->getConfigNames($configContent);
  111. }
  112. $this->out('' . count($configs) . ' of ' . $this->configCount . ' configs match:');
  113. $this->out('');
  114. $connections = array();
  115. if (!empty($configs)) {
  116. //$connections = array_keys($configs); # problems with 0 (mistake in shell::in())
  117. foreach ($configs as $key => $config) {
  118. $this->out(($key + 1) . ': ' . $config);
  119. $connections[] = $key + 1;
  120. }
  121. } else {
  122. $this->out('No configs found in /config/' . $this->configCustomFile . '!');
  123. }
  124. if (false) {
  125. /*
  126. if (count($connections) == 1) {
  127. $connection = 1;
  128. } elseif (isset($this->args[0])) {
  129. $tryConnection = array_search($this->args[0], $configs);
  130. if ($tryConnection !== false) {
  131. $connection = $tryConnection+1;
  132. }
  133. */
  134. } else {
  135. array_unshift($connections, 'q', 'h');
  136. $connection = $this->in(__('Use Sitecopy Config ([q] to quit, [h] for help)') . ':', $connections, 'q');
  137. }
  138. $this->out('');
  139. if (empty($connection) || $connection === 'q') {
  140. return $this->error('Aborted!');
  141. }
  142. if ($connection === 'h') {
  143. $this->help();
  144. return;
  145. }
  146. if (in_array($connection, $connections) && is_numeric($connection)) {
  147. $configuration = $this->getConfig($configs[$connection - 1], $configContent);
  148. //$this->typeName :: $this->types[$this->type]
  149. $configName = explode('_', $configs[$connection - 1], 2);
  150. if (!empty($configName[1])) {
  151. $this->configName = $configName[1];
  152. } else {
  153. return $this->error('Invalid config name \'' . $configs[$connection - 1] . '\'');
  154. }
  155. }
  156. # allow c, v and p only with app configs -> set params (by splitting app_configName into app and configName)
  157. if ($this->type > 3 || $this->type > 0 && $configName[0] !== 'app') {
  158. return $this->error('"-c" (-cake), "-v" (-vendor) and "-p" (-plugin) only possible with app configs (not with custom ones)');
  159. }
  160. if (empty($configuration)) {
  161. return $this->error('Error...');
  162. }
  163. $this->out('... Config \'' . $this->types[$this->type] . '_' . $this->configName . '\' selected ...');
  164. $hasLocalPath = false;
  165. $this->out('');
  166. # display global content (if available)
  167. if (!empty($this->configGlobal)) {
  168. //$this->out('GLOBAL CONFIG:');
  169. foreach ($this->configGlobal as $c) {
  170. if ($rF = $this->isRemotePath($c)) {
  171. $this->remoteFolder = $rF;
  172. } elseif ($this->isLocalPath($c)) {
  173. $hasLocalPath = true;
  174. }
  175. //$this->out($c);
  176. }
  177. }
  178. # display custom content
  179. //$this->out('CUSTOM CONFIG (may override global config):');
  180. $this->credentials = array();
  181. foreach ($configuration as $c) {
  182. if ($rF = $this->isRemotePath($c)) {
  183. $this->remoteFolder = $rF;
  184. } elseif ($lF = $this->isLocalPath($c)) {
  185. $this->localFolder = $lF;
  186. $hasLocalPath = true;
  187. } elseif ($cr = $this->areCredentials($c)) {
  188. $this->credentials[] = $cr;
  189. }
  190. }
  191. # "vendor" or "cake"? -> change both localFolder and remoteFolder and add them to to the config array
  192. if ($this->type > 0) {
  193. $configuration = $this->getConfig($this->types[$this->type], $configContent);
  194. //pr($configuration);
  195. $folder = $this->types[$this->type];
  196. if (!empty($this->matches[$this->type])) {
  197. $folder = $this->matches[$this->type];
  198. }
  199. # working with different OS - best to always use / slash
  200. $this->localFolder = dirname($this->localFolder) . DS . $folder;
  201. $this->localFolder = str_replace(DS, '/', $this->localFolder);
  202. $this->remoteFolder = dirname($this->remoteFolder) . DS . $folder;
  203. $this->remoteFolder = str_replace(DS, '/', $this->remoteFolder);
  204. foreach ($this->credentials as $c) {
  205. $configuration[] = $c;
  206. }
  207. $configuration[] = $this->localFolder;
  208. $configuration[] = $this->remoteFolder;
  209. }
  210. /*
  211. if (!$hasLocalPath) {
  212. # add the automatically found app folder as local path (default if no specific local path was given)
  213. $localPath = 'local '.TB.TB.$this->localFolder;
  214. $this->out($localPath);
  215. $configuration[] = $localPath;
  216. }
  217. */
  218. $this->tmpFile = 'config_' . $this->types[$this->type] . '_' . $this->configName . '.tmp';
  219. $this->logFile = 'log_' . $this->types[$this->type] . '_' . $this->configName . '.txt';
  220. # create tmp config file (adding the current APP path, of no local path was given inside the config file)
  221. $File = new File($this->tmpFolder . $this->tmpFile, true, 0770);
  222. //$File->open();
  223. $configTotal = array();
  224. # extract "side xyz" from config, add global and then the rest of custom
  225. $configTotal[] = 'site ' . $this->types[$this->type] . '_' . $this->configName;//$configuration[0];
  226. unset($configuration[0]);
  227. foreach ($this->configGlobal as $c) {
  228. $configTotal[] = $c;
  229. }
  230. foreach ($configuration as $c) {
  231. $configTotal[] = $c;
  232. }
  233. foreach ($configTotal as $key => $val) {
  234. $this->out($val);
  235. }
  236. $File->write(implode(NL, $configTotal), 'w', true);
  237. while (true) {
  238. $this->out('');
  239. $this->out('Type: ' . $this->types[$this->type]);
  240. $this->out('');
  241. $allowedActions = array('i', 'c', 'l', 'f', 'u', 's');
  242. if (isset($this->args[1])) {
  243. $action = strtolower(trim($this->args[1]));
  244. $this->args[1] = null; # only the first time
  245. } elseif (isset($this->args[0])) {
  246. if (mb_strlen(trim($this->args[0])) === 1) {
  247. $action = strtolower(trim($this->args[0]));
  248. }
  249. $this->args[0] = null; # only the first time
  250. }
  251. if (empty($action) || !in_array($action, $allowedActions)) {
  252. $action = strtolower($this->in(__('Init, Catchup, List, Fetch, Update, Synch (or [q] to quit)') . ':', array_merge($allowedActions, array('q')), 'l'));
  253. }
  254. if ($action === 'q') {
  255. return $this->error('Aborted!');
  256. }
  257. if (in_array($action, $allowedActions)) {
  258. # synch can destroy local information that might not have been saved yet, so confirm
  259. if ($action === 's') {
  260. $continue = $this->in(__('Local files might be overridden... Continue?'), array('y', 'n'), 'n');
  261. if (strtolower($continue) !== 'y' && strtolower($continue) !== 'yes') {
  262. $action = '';
  263. continue;
  264. }
  265. }
  266. $options = array();
  267. $options[] = '--show-progress';
  268. if (!empty($this->params['force'])) {
  269. $options[] = '--keep-going';
  270. }
  271. $name = $this->types[$this->type] . '_' . $this->configName;
  272. $this->_execute($name, $action, $options);
  273. }
  274. $action = '';
  275. }
  276. }
  277. /**
  278. * Only main functions covered - see "sitecopy --help" for more information
  279. */
  280. protected function _execute($config = null, $action = null, $options = array()) {
  281. $options[] = '--debug=ftp,socket --rcfile=' . $this->tmpFolder . $this->tmpFile .
  282. ' --storepath=' . $this->tmpFolder . ' --logfile=' . $this->tmpFolder . $this->logFile;
  283. if (!empty($action)) {
  284. if ($action === 'i') {
  285. $options[] = '--initialize';
  286. } elseif ($action === 'c') {
  287. $options[] = '--catchup';
  288. } elseif ($action === 'l') {
  289. $options[] = '--list';
  290. } elseif ($action === 'f') {
  291. $options[] = '--fetch';
  292. } elseif ($action === 'u') {
  293. $options[] = '--update';
  294. } elseif ($action === 's') {
  295. $options[] = '--synchronize';
  296. }
  297. }
  298. #last
  299. if (!empty($config)) {
  300. $options[] = $config;
  301. //pr($options);
  302. }
  303. $this->_exec(false, $options);
  304. # "Job Done"-Sound for the time comsuming actions (could be other sounds as well?)
  305. if ($action === 'f' || $action === 'u') {
  306. $this->_beep();
  307. }
  308. $this->out('... done ...');
  309. }
  310. /**
  311. * @return boolean isLocalPath (true/false)
  312. */
  313. protected function isLocalPath($line) {
  314. if (mb_strlen($line) > 7 && trim(mb_substr($line, 0, 6)) === 'local') {
  315. $config = trim(str_replace('local ', '', $line));
  316. if (!empty($config)) {
  317. return $config;
  318. }
  319. }
  320. return false;
  321. }
  322. /**
  323. * @return string path on success, boolean FALSE otherwise
  324. */
  325. protected function isRemotePath($line) {
  326. if (mb_strlen($line) > 8 && trim(mb_substr($line, 0, 7)) === 'remote') {
  327. $config = trim(str_replace('remote ', '', $line));
  328. if (!empty($config)) {
  329. return $config;
  330. }
  331. }
  332. return false;
  333. }
  334. /**
  335. * @return string path on success, boolean FALSE otherwise
  336. */
  337. protected function areCredentials($line) {
  338. if (mb_strlen($line) > 8) {
  339. if (trim(mb_substr($line, 0, 7)) === 'server') {
  340. $config = trim(str_replace('server ', '', $line));
  341. } elseif (trim(mb_substr($line, 0, 9)) === 'username') {
  342. $config = trim(str_replace('username ', '', $line));
  343. } elseif (trim(mb_substr($line, 0, 9)) === 'password') {
  344. $config = trim(str_replace('password ', '', $line));
  345. }
  346. if (!empty($config)) {
  347. return $config;
  348. }
  349. }
  350. return false;
  351. }
  352. /**
  353. * Make a small sound to inform the user accustically about the success.
  354. *
  355. * @return void
  356. */
  357. protected function _beep() {
  358. if ($this->params['silent']) {
  359. return;
  360. }
  361. # seems to work only on windows systems - advantage: sound does not need to be on
  362. $File = new File($this->scriptFolder . 'files' . DS . 'beep.bat');
  363. $sound = $File->read();
  364. system($sound);
  365. # seems to work on only on windows xp systems + where sound is on
  366. //$sound = 'sndrec32 /play /close "'.$this->scriptFolder.'files'.DS.'notify.wav';
  367. //system($sound);
  368. if (WINDOWS) {
  369. } else {
  370. exec('echo -e "\a"');
  371. }
  372. }
  373. /**
  374. * @return boolean Success
  375. */
  376. protected function _exec($silent = true, $options = array()) {
  377. # make sure, folder exists
  378. $Folder = new Folder($this->tmpFolder, true, 0770);
  379. $f = (WINDOWS ? $this->sitecopyFolder : '') . 'sitecopy ';
  380. $f .= implode(' ', $options);
  381. if (!empty($this->params['debug'])) {
  382. $this->hr();
  383. $this->out($f);
  384. $this->hr();
  385. return true;
  386. }
  387. if ($silent !== false) {
  388. $res = exec($f);
  389. return $res === 0;
  390. }
  391. $res = system($f);
  392. return $res !== false;
  393. }
  394. /**
  395. * Displays help contents
  396. *
  397. * @return void
  398. */
  399. public function help() {
  400. $this->hr();
  401. $this->out("Usage: cake copy <arg1> <arg2>...");
  402. $this->hr();
  403. $this->out('Commands [arg1]:');
  404. $this->out("\tName of Configuration (only neccessarry if there are more than 1)");
  405. $this->out("\nCommands [arg2]:");
  406. $this->out("\ti: Init (Mark all files and directories as not updated)");
  407. $this->out("\tc: Catchup (Mark all files and directories as updated)");
  408. $this->out("\tl: List (Show differences)");
  409. $this->out("\tf: Fetch (Get differences)");
  410. $this->out("\tu: Update (Copy local content to remote location)");
  411. $this->out("\ts: Synch (Get remote content and override local files");
  412. $this->out("");
  413. $continue = $this->in(__('Show script help, too?'), array('y', 'n'), 'y');
  414. if (strtolower($continue) === 'y' || strtolower($continue) === 'yes') {
  415. # somehow does not work yet (inside cake console anyway...)
  416. $this->_exec(false, array('--help'));
  417. $this->out('');
  418. $this->_exec(false, array('--version'));
  419. }
  420. return $this->_stop();
  421. }
  422. /**
  423. * Read out config file and parse it to an array
  424. */
  425. protected function getConfigs() {
  426. # global file (may be present)
  427. $File = new File($this->localFolder . 'config' . DS . $this->configGlobalFile);
  428. if ($File->exists()) {
  429. $File->open('r');
  430. $content = (string)$File->read();
  431. $content = explode(NL, $content);
  432. if (!empty($content)) {
  433. $configGlobal = array();
  434. foreach ($content as $line => $c) {
  435. $c = trim($c);
  436. if (!empty($c)) {
  437. $configGlobal[] = $c;
  438. }
  439. }
  440. $this->configGlobal = $configGlobal;
  441. }
  442. }
  443. # custom file (must be present)
  444. $File = new File($this->localFolder . 'config' . DS . $this->configCustomFile);
  445. if (!$File->exists()) {
  446. return $this->error('No config file present (/config/' . $this->configCustomFile . ')!');
  447. }
  448. $File->open('r');
  449. # Read out configs
  450. $content = $File->read();
  451. if (empty($content)) {
  452. return array();
  453. }
  454. $content = explode(NL, $content);
  455. if (empty($content)) {
  456. return array();
  457. }
  458. $configContent = array();
  459. foreach ($content as $line => $c) {
  460. $c = trim($c);
  461. if (!empty($c)) {
  462. $configContent[] = $c;
  463. }
  464. }
  465. return $configContent;
  466. }
  467. /**
  468. * Get a list with available configs
  469. *
  470. * @param array $content
  471. * checks on whether all config names are valid!
  472. */
  473. protected function getConfigNames($content) {
  474. $configs = array();
  475. foreach ($content as $c) {
  476. if (mb_strlen($c) > 6 && trim(mb_substr($c, 0, 5)) === 'site') {
  477. $config = trim(str_replace('site ', '', $c));
  478. if (!empty($config)) {
  479. if (!$this->isValidConfigName($config)) {
  480. return $this->error('Invalid Config Name \'' . $config . '\' in /config/' . $this->configCustomFile . '!' . NL . 'Allowed: [app|custom]+\'_\'+{a-z0-9-} or [cake|vendor|plugin]');
  481. }
  482. if ($this->typeMatchesConfigName($config, $this->type)) {
  483. $configs[] = $config;
  484. }
  485. $this->configCount++;
  486. }
  487. }
  488. }
  489. return $configs;
  490. }
  491. /**
  492. * Makes sure nothing strange happens if there is an invalid config name
  493. * (like updating right away on "cake copy u", if u is supposed to be the config name...)
  494. */
  495. protected function isValidConfigName($name) {
  496. $reservedWords = array('i', 'c', 'l', 'f', 'u', 's');
  497. if (in_array($name, $reservedWords)) {
  498. return false;
  499. }
  500. if (!preg_match(VALID_ALPHANUMERIC_HYPHEN_UNDERSCORE, $name)) {
  501. return false;
  502. }
  503. if ($name !== 'cake' && $name !== 'vendor' && $name !== 'plugin' && substr($name, 0, 4) !== 'app_' && substr($name, 0, 7) !== 'custom_') {
  504. return false;
  505. }
  506. return true;
  507. }
  508. /**
  509. * Makes sure type matches config name (app = only app configs, no cake or vendor or custom configs!)
  510. *
  511. * @return string type on success, otherwise boolean false
  512. */
  513. protected function typeMatchesConfigName($name, $type) {
  514. if (array_key_exists($type, $this->types) && $name !== 'cake' && $name !== 'vendor' && $name !== 'plugin') {
  515. $splits = explode('_', $name, 2); # cake_eee_sss = cake & eee_sss
  516. if (!empty($splits[0]) && in_array(trim($splits[0]), $this->types)) {
  517. return true;
  518. }
  519. }
  520. return false;
  521. }
  522. /**
  523. * Return the specific config of a config name
  524. *
  525. * @param string $config name
  526. * @param array $content
  527. */
  528. protected function getConfig($config, $content) {
  529. $configs = array();
  530. $started = false;
  531. foreach ($content as $c) {
  532. if (mb_strlen($c) > 6 && substr($c, 0, 5) === 'site ') {
  533. $currentConfig = trim(str_replace('site ', '', $c));
  534. if (!empty($currentConfig) && $currentConfig == $config) {
  535. # start
  536. if (!$started) {
  537. # prevent problems with 2 configs with the same alias (but shouldnt happen anyway)
  538. $currentConfig = null;
  539. }
  540. $started = true;
  541. }
  542. }
  543. if ($started && !empty($currentConfig)) {
  544. # done
  545. break;
  546. }
  547. if ($started) {
  548. $configs[] = $c;
  549. }
  550. }
  551. return $configs;
  552. }
  553. public function getOptionParser() {
  554. $subcommandParser = array(
  555. 'options' => array(
  556. /*
  557. 'plugin' => array(
  558. 'short' => 'g',
  559. 'help' => __d('cake_console', 'The plugin to update. Only the specified plugin will be updated.'),
  560. 'default' => ''
  561. ),
  562. 'dry-run'=> array(
  563. 'short' => 'd',
  564. 'help' => __d('cake_console', 'Dry run the update, no files will actually be modified.'),
  565. 'boolean' => true
  566. ),
  567. 'log'=> array(
  568. 'short' => 'l',
  569. 'help' => __d('cake_console', 'Log all ouput to file log.txt in TMP dir'),
  570. 'boolean' => true
  571. ),
  572. */
  573. 'silent' => array(
  574. 'short' => 's',
  575. 'help' => __d('cake_console', 'Silent mode (no beep sound)'),
  576. 'boolean' => true
  577. ),
  578. 'vendors' => array(
  579. 'short' => 'e',
  580. 'help' => __d('cake_console', 'ROOT/vendor'),
  581. 'boolean' => true
  582. ),
  583. 'cake' => array(
  584. 'short' => 'c',
  585. 'help' => __d('cake_console', 'ROOT/lib/Cake'),
  586. 'boolean' => true
  587. ),
  588. 'app' => array(
  589. 'short' => 'a',
  590. 'help' => __d('cake_console', 'ROOT/app'),
  591. 'boolean' => true
  592. ),
  593. 'plugins' => array(
  594. 'short' => 'p',
  595. 'help' => __d('cake_console', 'ROOT/plugin'),
  596. 'boolean' => true
  597. ),
  598. 'custom' => array(
  599. 'short' => 'u',
  600. 'help' => __d('cake_console', 'custom'),
  601. 'boolean' => true
  602. ),
  603. 'force' => array(
  604. 'short' => 'f',
  605. 'help' => __d('cake_console', 'force (keep going regardless of errors)'),
  606. 'boolean' => true
  607. ),
  608. 'debug' => array(
  609. 'help' => __d('cake_console', 'Debug output only'),
  610. 'boolean' => true
  611. )
  612. )
  613. );
  614. return parent::getOptionParser()
  615. ->description(__d('cake_console', "A shell to quickly upload modified files (diff) only."))
  616. ->addSubcommand('run', array(
  617. 'help' => __d('cake_console', 'Update'),
  618. 'parser' => $subcommandParser
  619. ));
  620. }
  621. }