CopyShell.php 20 KB

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