CopyShell.php 19 KB

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