UNPKG

34.1 kBJavaScriptView Raw
1// jscs:disable jsDoc
2/**
3 * This code is closed source and Confidential and Proprietary to
4 * Appcelerator, Inc. All Rights Reserved. This code MUST not be
5 * modified, copied or otherwise redistributed without express
6 * written permission of Appcelerator. This file is licensed as
7 * part of the Appcelerator Platform and governed under the terms
8 * of the Appcelerator license agreement.
9 */
10var fs = require('fs'),
11 path = require('path'),
12 os = require('os'),
13 chalk,
14 urllib = require('url'),
15 PacAgent = require('pac-proxy-agent'),
16 semver = require('semver'),
17 debug = require('debug')('appc:util'),
18 spinner,
19 spriteIndex = 0,
20 cachedConfig,
21 sprite = '/-\\|',
22 execSync = require('child_process').execSync; // eslint-disable-line security/detect-child-process
23
24var MAX_RETRIES = exports.MAX_RETRIES = 5;
25var CONN_TIMEOUT = 10000;
26
27// NOTE: not using char-spinner because i don't want it to reset to beginning
28// of line each time it starts/stops. i want to spin in place at point of cursor
29
30/*
31 Testing Utilities.
32 */
33exports.stdout = process.stdout;
34/* istanbul ignore next */
35exports.exit = function (code) {
36 process.exit(code);
37};
38/* istanbul ignore next */
39exports.setCachedConfig = function (val) {
40 cachedConfig = val;
41};
42
43/**
44 * start the spinner
45 */
46function startSpinner() {
47 stopSpinner();
48 if (!spinner && exports.stdout.isTTY && !process.env.TRAVIS) {
49 var count = 0;
50 spinner = setInterval(function () {
51 if (count++ > 0) {
52 // go back one column
53 exports.stdout.write('\u001b[1D');
54 }
55 var s = ++spriteIndex % sprite.length;
56 var c = sprite[s];
57 exports.stdout.write(c);
58 }, 50);
59 }
60}
61
62/**
63 * stop the spinner
64 */
65function stopSpinner() {
66 if (spinner) {
67 clearInterval(spinner);
68 // go back on column
69 exports.stdout.write('\u001b[1D');
70 spinner = null;
71 }
72}
73
74/**
75 * write a wait message and start spinner
76 * @param {string} msg - message to output
77 */
78function waitMessage(msg) {
79 exports.stdout.write(msg);
80 startSpinner();
81}
82
83/**
84 * write OK mark and stop spinner
85 * @param {string} msg - message to output
86 */
87function okMessage(msg) {
88 chalk = chalk || require('chalk');
89 stopSpinner();
90 msg = msg || '';
91 exports.stdout.write(msg + ' ' + chalk.green(isWindows() ? 'OK' : '✓') + '\n');
92}
93
94/**
95 * write message and stop spinner
96 * @param {string} msg - message to output
97 */
98function infoMessage(msg) {
99 stopSpinner();
100 exports.stdout.write(msg + '\n');
101}
102
103/**
104 * return the platform specific HOME directory
105 * @returns {string}
106 */
107function getHomeDir() {
108 return os.homedir();
109}
110
111/**
112 * return our AppC directory
113 * @returns {string}
114 */
115function getAppcDir() {
116 return path.join(getHomeDir(), '.appcelerator');
117}
118
119/**
120 * return the AppC install tag file
121 * @returns {string}
122 */
123function getInstallTag() {
124 return path.join(getAppcDir(), '.installing');
125}
126
127/**
128 * return the global cache directory in the users home folder
129 * @returns {string}
130 */
131function getCacheDir() {
132 return path.join(getAppcDir(), 'cache');
133}
134
135/**
136 * return the platform specific install directory
137 * @returns {string}
138 */
139function getInstallDir() {
140 return path.join(getAppcDir(), 'install');
141}
142
143/**
144 * return the version file
145 * @returns {string}
146 */
147function getVersionFile() {
148 return path.join(getInstallDir(), '.version');
149}
150
151/**
152 * return the config file
153 * @returns {string}
154 */
155function getConfigFile() {
156 return path.join(getAppcDir(), 'appc-cli.json');
157}
158
159/**
160 * return the private npm cache directory
161 * @returns {string}
162 */
163function getNpmCacheDirectory() {
164 return path.join(getAppcDir(), '.npm');
165}
166
167/**
168 * write out the current version file
169 * @param {string} version - Version to write
170 */
171function writeVersion(version) {
172 var versionFile = getVersionFile();
173 debug('writing version: %s to %s', version, versionFile);
174 if (fs.existsSync(versionFile)) {
175 fs.unlinkSync(versionFile);
176 }
177 fs.writeFileSync(versionFile, version);
178}
179
180/**
181 * return the active version (if specified) or undefined
182 * @returns {string|undefined}
183 */
184function getActiveVersion() {
185 var versionFile = getVersionFile();
186 if (fs.existsSync(versionFile)) {
187 return fs.readFileSync(versionFile).toString().trim();
188 }
189}
190
191/**
192 * remove version file
193 */
194function removeVersion() {
195 var versionFile = getVersionFile();
196 debug('remove version %s', versionFile);
197 if (fs.existsSync(versionFile)) {
198 fs.unlinkSync(versionFile);
199 }
200}
201
202/**
203 * list versions installed
204 * @param {object} opts - Options
205 * @param {object} versions - Versions of the CLI
206 * @returns {undefined}
207 */
208function listVersions(opts, versions) {
209 chalk = chalk || require('chalk');
210 if (!versions) {
211 exports.stdout.write(chalk.red('No versions available') + '\n');
212 return;
213 }
214 var activeVersion = getActiveVersion();
215
216 // If we don't find latest version from api/appc/list endpoint, then inject the latest version into the list.
217 var versionsList = Object.keys(versions).map(function (value, index) {
218 return versions[index].version || versions[index];
219 });
220
221 if (opts.latest && versionsList.indexOf(opts.latest) === -1) {
222 versionsList.push(opts.latest);
223 }
224
225 versions = versionsList.sort(semver.compareLoose);
226
227 versions.forEach(function (entry) {
228 var ver = entry.version ? entry.version : entry,
229 suffix = getInstallBinary(opts, ver) ? 'Installed' : 'Not Installed';
230 if (opts.latest === ver) {
231 suffix += chalk.white.bold(' (Latest)');
232 }
233 if (activeVersion && activeVersion === ver) {
234 suffix += chalk.red(' (Active)');
235 }
236 var date = entry.date ? ' ' + chalk.grey(pad(new Date(Date.parse(entry.date)), 15)) : '';
237 exports.stdout.write(chalk.yellow(pad(ver, 10)) + ' ' + chalk.cyan(pad(suffix, 40)) + date + '\n');
238 });
239}
240
241/**
242 * return json object of versions
243 * @param {object} opts - Options
244 * @param {object} versions - CLI versions
245 * @return {object}
246 */
247function getVersionJson(opts, versions) {
248 var activeVersion = getActiveVersion(),
249 obj = {
250 versions: [],
251 installed: getInstalledVersions(),
252 latest: opts.latest,
253 active: activeVersion
254 };
255 if (Array.isArray(versions)) {
256 if (versions[0] && versions[0].version) {
257 obj.versions = Object.keys(versions).map(function (value, index) {
258 return versions[index].version;
259 });
260 } else {
261 obj.versions = versions;
262 }
263 }
264 return obj;
265}
266
267/**
268 * return the current versions installed
269 * @returns {string[]}
270 */
271function getInstalledVersions() {
272 var installDir = getInstallDir();
273 if (fs.existsSync(installDir)) {
274 // try and resolve the latest
275 try {
276 var dirs = fs.readdirSync(installDir);
277 if (dirs.length) {
278 if (dirs.length > 1) {
279 // attempt to sort by latest version
280 dirs = dirs
281 .filter(function (e) {
282 return e[0] !== '.';
283 })
284 .sort(function (a, b) {
285 var av = parseInt(a.replace(/\./g, '')),
286 bv = parseInt(b.replace(/\./g, ''));
287 return bv - av;
288 });
289 }
290 debug('found the following version directories: %j', dirs);
291 return dirs;
292 }
293 } catch (E) {
294 debug('error reading install directory %o', E);
295 if (E.code === 'EACCES') {
296 chalk = chalk || require('chalk');
297 var chPer = 'Please make sure you change the permissions and re-try';
298 var chPerWithCmds = 'Please make sure you change the permissions using these commands:\n\n\t';
299 chPerWithCmds += chalk.yellow('sudo chown -R ' + process.env.USER + ' ' + installDir + '\n\tchmod -R 0700 ' + installDir);
300 var message = process.platform === 'win32' ? chPer : chPerWithCmds + '\n';
301 fail('Ooops! Your install directory (' + installDir + ') is not writable.\n' + message);
302 }
303 fail(E);
304 }
305 }
306}
307
308/**
309 * return the platform specific install binary path
310 * @param {object} opts - Options
311 * @param {string} theversion - version to lookup the install binary for
312 * @returns {string|null}
313 */
314function getInstallBinary(opts, theversion) {
315 opts = opts || {};
316 // first check and see if specified on command line as option
317 var version = theversion || (opts.version !== true ? opts.version : null) || '',
318 installDir = getInstallDir(),
319 bin = path.join(installDir, version, 'package', 'bin', 'appc'),
320 pkg,
321 dirs;
322
323 if (fs.existsSync(bin)) {
324 // check the package.json since we will delete it on an interrupted download attempt
325 pkg = path.join(installDir, version, 'package', 'package.json');
326 return fs.existsSync(pkg) && bin;
327 } else if (theversion) {
328 // if we specified a version and we didn't find it, return null
329 return null;
330 }
331
332 // see if we have a version set
333 theversion = getActiveVersion();
334 if (theversion) {
335 bin = getInstallBinary(opts, theversion);
336 if (!bin) {
337 if (!opts.version) {
338 chalk = chalk || require('chalk');
339 debug('you have specified a version (%s) that isn\'t found', theversion);
340 // only warn if we're not asking for this version
341 // invalid version specified in version file. remove it and then re-install from latest
342 exports.stdout.write(chalk.red('version ' + theversion + ' specified previously is no longer available.') + '\n');
343 }
344 removeVersion();
345 } else {
346 return bin;
347 }
348 }
349
350 dirs = getInstalledVersions();
351 if (dirs) {
352 for (var c = 0; c < dirs.length; c++) {
353 bin = path.join(installDir, dirs[c], 'package', 'bin', 'appc');
354 if (fs.existsSync(bin)) {
355 // check the package.json since we will delete it on an interrupted download attempt
356 pkg = path.join(installDir, dirs[c], 'package', 'package.json');
357 return fs.existsSync(pkg) && bin;
358 }
359 }
360 }
361}
362
363/**
364 * given a full path, makes sure that the directory exists
365 * @param {string} dir - Directory to ensure exists
366 * @return {string}
367 */
368function ensureDir(dir) {
369 var last = expandPath(dir),
370 parts = [];
371 // find the top of the root that exists
372 do {
373 parts.unshift(path.basename(last));
374 last = path.join(last, '..');
375 } while (!fs.existsSync(last));
376
377 if (!fs.existsSync(last)) {
378 fs.mkdirSync(last);
379 }
380
381 // now create the directories in order
382 for (var c = 0; c < parts.length; c++) {
383 var fp = path.join(last, parts[c]);
384 if (!fs.existsSync(fp)) {
385 fs.mkdirSync(fp);
386 }
387 last = fp;
388 }
389
390 return dir;
391}
392
393/**
394 * expand ~ in fn
395 * @param {string} fn - Path to expand
396 * @returns {string}
397 */
398function expandPath(fn) {
399 var home = getHomeDir(),
400 p = fn.replace(/~\/?/g, function (value) {
401 if (/\/$/.test(value)) {
402 return home + '/';
403 }
404 return home;
405 });
406 return p;
407}
408
409/**
410 * fail and properly exit
411 * @param {string} msg - Message to output
412 */
413function fail(msg) {
414 stopSpinner();
415 debug('fail %o', msg);
416 if (msg.stack && process.env.DEBUG) {
417 console.error(msg.stack);
418 }
419 chalk = chalk || require('chalk');
420 console.error('\n' + (chalk.red(msg.message || msg)));
421 exports.exit(1);
422}
423
424var optionRE = /^-{1,2}([\w-_]+)=?(.*)?$/;
425
426/**
427 * very loose parsing of options
428 * @returns {object}
429 */
430function parseOpts() {
431 var args = {};
432 for (var c = 2; c < process.argv.length; c++) {
433 var arg = process.argv[c];
434 if (optionRE.test(arg)) {
435 var match = optionRE.exec(arg),
436 name = match[1],
437 value = match.length > 2 && match[2] || (process.argv[c + 1] && !/^-{1,2}/.test(process.argv[c + 1]) ? process.argv[c + 1] : null) || true;
438 if (value === 'true' || value === 'false') {
439 value = value === 'true';
440 }
441 if (name.indexOf('no-') === 0) {
442 name = name.substring(3);
443 value = false;
444 }
445 args[name] = value;
446 }
447 }
448 return args;
449}
450
451/**
452 * loose parse none options
453 * @param {object} opts - Options
454 * @returns {string[]}
455 */
456function parseArgs(opts) {
457 if (!opts) {
458 throw new Error('missing opts');
459 }
460 var args = [];
461 for (var c = 2; c < process.argv.length; c++) {
462 var arg = process.argv[c];
463 var previous = process.argv[c - 1];
464 if (optionRE.test(previous)) {
465 var previousMatch = optionRE.exec(previous);
466 previous = previousMatch[1];
467 }
468 if (optionRE.test(arg)) {
469 var match = optionRE.exec(arg),
470 name = match[1],
471 value = opts[name];
472 // see if a value was provided and if so, remove it too
473 if (value && String(process.argv[c + 1] === String(value))) {
474 c++;
475 }
476 continue;
477 } else if (opts[previous] === undefined) {
478 args.push(arg);
479 }
480 }
481 return args;
482}
483
484/**
485 * make a registry url
486 * @param {object} opts - Options
487 * @param {string} urlpath - Path to add to the the baseUrl
488 * @returns {string}
489 */
490function makeURL(opts, urlpath) {
491 if (typeof (opts) === 'string') {
492 urlpath = opts;
493 opts = {};
494 } else {
495 opts = opts || {};
496 }
497 var baseurl;
498 if (opts.registry) {
499 baseurl = opts.registry;
500 } else if (process.env.APPC_REGISTRY_SERVER) {
501 baseurl = process.env.APPC_REGISTRY_SERVER;
502 } else if (process.env.APPC_ENV || process.env.NODE_ENV) {
503 var env = process.env.APPC_ENV || process.env.NODE_ENV;
504 if (env === 'preproduction') {
505 baseurl = DEFAULT_PREPROD_REGISTRY_URL;
506 } else if (env === 'preprodonprod') {
507 baseurl = DEFAULT_PREPRODONPROD_REGISTRY_URL;
508 } else if (env === 'production') {
509 baseurl = DEFAULT_PROD_REGISTRY_URL;
510 }
511 }
512 if (!baseurl) {
513 var config = exports.readConfig();
514 if (config && config.registry) {
515 baseurl = config.registry;
516 } else if (config && (config.defaultEnvironment === 'preproduction' || config.environmentName === 'preproduction')) {
517 baseurl = DEFAULT_PREPROD_REGISTRY_URL;
518 } else if (config && (config.defaultEnvironment === 'preprodonprod' || config.environmentName === 'preprodonprod')) {
519 baseurl = DEFAULT_PREPRODONPROD_REGISTRY_URL;
520 } else {
521 baseurl = DEFAULT_PROD_REGISTRY_URL;
522 }
523 }
524 return urllib.resolve(baseurl, urlpath);
525}
526
527function makeRequestError(msg, code) {
528 var err = new Error(msg);
529 err.code = code;
530 return err;
531}
532
533// TODO: Perhaps we should include and use appc-platform-sdk env vars?
534var DEFAULT_PREPROD_REGISTRY_URL = 'https://registry.axwaytest.net';
535var DEFAULT_PROD_REGISTRY_URL = 'https://registry.platform.axway.com';
536var DEFAULT_PREPRODONPROD_REGISTRY_URL = 'https://software-preprodonprod.appcelerator.com';
537
538/**
539 * return the request library
540 * @return {object}
541 */
542function getRequest() {
543 return require('request');
544}
545
546/**
547 * make a request to location url
548 * @param {string|object} location - url to request or request object
549 * @param {function} callback - function to call when done
550 * @returns {request}
551 */
552function request(location, callback) {
553 var options;
554 if (typeof (location) === 'object') {
555 options = location;
556 location = options.url;
557 }
558
559 var url = urllib.parse(location),
560 config = readConfig(),
561 userAgent = 'Appcelerator CLI/' + require('../package').version + ' (' + process.platform + ')',
562 opts = {
563 url: url,
564 headers: {
565 'user-agent': userAgent,
566 host: url.host,
567 'appc-token': config && config.sid
568 }
569 };
570
571 if (options) {
572 opts.timeout = CONN_TIMEOUT * (options.attempts || 1);
573 }
574
575 if (process.env.APPC_CONFIG_PAC_FILE) {
576 opts.agent = new PacAgent('pac+' + process.env.APPC_CONFIG_PAC_FILE);
577 } else if (process.env.APPC_CONFIG_PROXY !== '') {
578 opts.proxy = process.env.APPC_CONFIG_PROXY;
579 }
580
581 if (process.env.APPC_CONFIG_CAFILE) {
582 opts.ca = fs.readFileSync(process.env.APPC_CONFIG_CAFILE, 'utf8');
583 }
584
585 if (process.env.APPC_CONFIG_STRICTSSL === 'false') {
586 opts.strictSSL = false;
587 }
588
589 var req = getRequest().get(opts);
590
591 debug('request %j', opts);
592
593 // start the request
594 req.on('response', function (res) {
595 debug('request response received');
596 if (req.__err) {
597 debug('request response callback skipped, request error already executed.');
598 } else {
599 callback(null, res, req);
600 }
601 });
602
603 // check the error
604 req.on('error', function (err) {
605 req.__err = true;
606 debug('request error', err);
607 if (err.name === 'AppCError') {
608 return callback(err);
609 }
610 if (err.code === 'ECONNREFUSED') {
611 return callback(makeRequestError('Error connecting to download server at ' + url.host + '. Make sure you are online.', err.code));
612 } else if (err.code === 'ENOTFOUND') {
613 return callback(makeRequestError('Error connecting to download server at ' + url.host + ' (not found). Make sure you are online.', err.code));
614 } else if (err.code === 'ECONNRESET') {
615 return callback(makeRequestError('Error connecting to download server at ' + url.host + ' (reset). Make sure you are online.', err.code));
616 }
617 return callback(err);
618 });
619
620 return req;
621}
622
623/**
624 * make a request a return JSON
625 * @param {string} location - URL to request
626 * @param {function} callback - Function to call when done
627 * @param {number} attempts - Number or attempts to make
628 * @return {request}
629 */
630function requestJSON(location, callback, attempts) {
631 return request(location, function (err, res, req) {
632 attempts = attempts || 1;
633 if (typeof (location) === 'object') {
634 location.attempts = attempts + 1;
635 }
636
637 debug('connection attempt %d of %d', attempts, MAX_RETRIES);
638 if (err) {
639 if (err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.message.indexOf('hang up') > 0) {
640 debug('connection error %s with message %s', err.code, err.message);
641 // retry again
642 if (attempts >= MAX_RETRIES) {
643 return callback(err);
644 }
645 return setTimeout(function () {
646 return requestJSON(location, callback, attempts + 1);
647 }, 500 * attempts);
648 }
649 return callback(err);
650 }
651 if (res && req.headers && req.headers['content-type'] && req.headers['content-type'].indexOf('/json') < 0) {
652 debug('response status code: %d with headers: %j', res.statusCode, res.headers);
653 // retry again
654 if (attempts >= MAX_RETRIES) {
655 return callback(err);
656 }
657 return setTimeout(function () {
658 return requestJSON(location, callback, attempts + 1);
659 }, 500 * attempts);
660 }
661 if (res.statusCode === 200) {
662 debug('response status code: %d with headers: %j', res.statusCode, res.headers);
663 var buf = '';
664 res.on('data', function (chunk) {
665 buf += chunk;
666 });
667 res.on('end', function () {
668 debug('attempting to parse JSON => [%s]', buf);
669 callback(null, JSON.parse(buf));
670 });
671 res.on('error', callback);
672 } else if (res.statusCode === 301 || res.statusCode === 302) {
673 debug('response status code: %d with headers: %j', res.statusCode, res.headers);
674 return requestJSON(res.headers.location, callback);
675 } else if (res && /^(400|404|408|500|502|503|504)$/.test(String(res.statusCode))) {
676 debug('response status code: %d with headers: %j', res.statusCode, res.headers);
677 attempts = attempts || 1;
678 if (attempts >= MAX_RETRIES) {
679 return callback(err);
680 }
681 return setTimeout(function () {
682 return requestJSON(location, callback, attempts + 1);
683 }, 500 * attempts);
684 } else {
685 return callback(new Error('Invalid response code: ' + res.statusCode + ' received from server.'));
686 }
687 });
688}
689
690/**
691 * right pad a string to a specific length
692 * @param {string} str - string to pad
693 * @param {number} len - number of spaces to pad
694 * @returns {string}
695 */
696function pad(str, len) {
697 chalk = chalk || require('chalk');
698 var slen = chalk.stripColor(str).length;
699 var newstr = str;
700 for (var c = slen; c < len; c++) {
701 newstr += ' ';
702 }
703 return newstr;
704}
705
706/**
707 * returns true if directory is writable by user
708 * @param {string} dir - Directory to check
709 * @returns {boolean}
710 */
711function canWriteDir(dir) {
712 var del = !fs.existsSync(dir),
713 fn;
714 try {
715 if (del) {
716 ensureDir(dir);
717 }
718 if (fs.statSync(dir).isDirectory()) {
719 // create a temp file -- seems like the best way to handle cross platform
720 fn = path.join(dir, String(+new Date()) + (Math.random() * 3) + '.txt');
721 // console.log(fn);
722 fs.writeFileSync(fn, 'hi');
723 return true;
724 } else {
725 // not a directory but a file, sorry...
726 return false;
727 }
728 } catch (E) {
729 if (E.code === 'EACCES') {
730 return false;
731 }
732 console.log(E.stack, E.code);
733 } finally {
734 if (fs.existsSync(fn)) {
735 try {
736 fs.unlinkSync(fn);
737 } catch (ig) {
738 // ignore
739 }
740 }
741 if (del) {
742 try {
743 fs.unlinkSync(path);
744 } catch (ig) {
745 // ignore
746 }
747 }
748 }
749}
750
751/**
752 * if not writable, returns a message otherwise undefined
753 * @param {string} dir - Directory to check
754 * @param {string} name - Error name
755 * @returns {undefined|AppcError}
756 */
757function checkDirectory(dir, name) {
758 var message;
759 var errorlib = require('./error'),
760 chalk = chalk || require('chalk');
761 if (!canWriteDir(dir)) {
762 var chPer = 'Please make sure you change the permissions and re-try';
763 var chPerWithCmd = 'Please make sure you change the permissions using these commands:\n\n\t';
764 chPerWithCmd += chalk.yellow('sudo chown -R ' + process.env.USER + ' ' + dir + '\n\tchmod -R 0700 ' + dir);
765 message = process.platform === 'win32' ? chPer : chPerWithCmd + '\n';
766 return errorlib.createError('com.appcelerator.install.preflight.directory.unwritable', name, dir, message);
767 } else if (process.platform !== 'win32') {
768 // check the ownership of the directory too
769 var stat = fs.statSync(dir);
770 if (stat.uid !== process.getuid()) {
771 message = 'Please make sure you change the permissions using these commands:\n\n\t' + chalk.yellow('sudo chown -R ' + process.env.USER + ' ' + dir + '\n\tchmod -R 0700 ' + dir) + '\n';
772 return errorlib.createError('com.appcelerator.install.preflight.directory.ownership', name, dir, process.env.USER, message);
773 }
774 }
775}
776
777function abortMessage(name) {
778 // clear line and reset it
779 if (exports.stdout.isTTY) {
780 stopSpinner();
781 exports.stdout.clearLine();
782 exports.stdout.cursorTo(0);
783 }
784 exports.stdout.write(name + ' aborted.\n');
785 exports.exit(1);
786}
787
788function readConfig() {
789 if (cachedConfig) {
790 return cachedConfig;
791 }
792 var cf = getConfigFile();
793 if (!fs.existsSync(cf)) {
794 return null;
795 }
796 return (cachedConfig = JSON.parse(fs.readFileSync(cf)));
797}
798
799function writeConfig(config) {
800 cachedConfig = config;
801 var cf = getConfigFile();
802 fs.writeFileSync(cf, JSON.stringify(config, null, 2));
803}
804
805/**
806 * perform an update check to see if we have a new version
807 *
808 * however, some rules:
809 *
810 * - don't check each time
811 * - if specifying a version, skip
812 * - if specifying --quiet, skip
813 * - if no config, skip
814 * - if any failure in checking, skip
815 * - only do it once per day (or what is configured)
816 *
817 * @param {object} opts - Options
818 * @param {function} callback - Function to call when done
819 * @returns {undefined}
820 */
821function updateCheck(opts, callback) {
822 // we are specifying a version or we want quiet output, skip
823 if (opts.version || opts.quiet || opts.output === 'json') {
824 return callback();
825 }
826
827 // check to see if we have a config file and if we don't that's OK, return
828 // since we are in a setup/install
829 var config = readConfig();
830
831 if (!config) {
832 return callback();
833 }
834
835 chalk = chalk || require('chalk');
836
837 try {
838 var check = config.lastUpdateCheck;
839 var checkEveryMS = config.updateCheckInterval || 86400000; // once per day in MS
840 if (!check || check + checkEveryMS < Date.now()) {
841 // do the check below
842 debug('update check skipping, %d, %d', check, checkEveryMS);
843 } else {
844 // don't do the check
845 return callback();
846 }
847 } catch (E) {
848 // ignore errors, they will be dealt with otherwise
849 return callback();
850 }
851
852 var url = makeURL(opts, '/api/appc/list');
853 exports.requestJSON(url, function (err, result) {
854 // skip failures
855 if (!err && result) {
856 try {
857 var activeVersion = exports.getActiveVersion(),
858 resultList = result.key && result[result.key],
859 latest = result.latest || (resultList && (resultList.length > 0) && resultList[0].version);
860
861 debug('update check completed, latest is %s', latest);
862 // set the update check timestamp
863 config.lastUpdateCheck = Date.now();
864
865 // write out our config
866 writeConfig(config);
867
868 // see if we have it already
869 var found = exports.getInstallBinary(opts, latest);
870
871 // if not, inform the user of the update
872 debug('update check found %s', found);
873 if (!found && semver.lt(activeVersion, latest)) {
874 exports.stdout.write('A new update ' + chalk.yellow('(' + latest + ')') + ' is available... Download with ' + chalk.green('appc use ' + latest) + '\n');
875 }
876 } catch (E) {
877 // ignore
878 }
879 }
880 callback();
881 });
882}
883
884function isWindows() {
885 return process.platform === 'win32';
886}
887
888/**
889 * if a TTY is connected, clear all text on the line and reset the
890 * cursor to the beginning of the line
891 */
892function resetLine() {
893 if (exports.stdout.isTTY) {
894 exports.stdout.clearLine();
895 exports.stdout.cursorTo(0);
896 }
897}
898
899/**
900 * rmdirSyncRecursive method borrowed from wrench
901 *
902 * The MIT License
903 *
904 * Copyright (c) 2010 Ryan McGrath
905 *
906 * Permission is hereby granted, free of charge, to any person obtaining a copy
907 * of this software and associated documentation files (the "Software"), to deal
908 * in the Software without restriction, including without limitation the rights
909 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
910 * copies of the Software, and to permit persons to whom the Software is
911 * furnished to do so, subject to the following conditions:
912 *
913 * The above copyright notice and this permission notice shall be included in
914 * all copies or substantial portions of the Software.
915 *
916 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
917 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
918 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
919 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
920 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
921 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
922 * THE SOFTWARE.
923 *
924 * @param {string} _path - filepath
925 * @param {boolean} failSilent - Fail silently
926 * @returns {undefined}
927 */
928function rmdirSyncRecursive(_path, failSilent) {
929 var files;
930
931 try {
932 files = fs.readdirSync(_path);
933 } catch (err) {
934
935 if (failSilent) {
936 return;
937 }
938 throw new Error(err.message);
939 }
940
941 /* Loop through and delete everything in the sub-tree after checking it */
942 for (var i = 0; i < files.length; i++) {
943 var file = path.join(_path, files[i]);
944 var currFile = fs.lstatSync(file);
945
946 if (currFile.isDirectory()) {
947 // Recursive function back to the beginning
948 rmdirSyncRecursive(file);
949 } else if (currFile.isSymbolicLink()) {
950 // Unlink symlinks
951 if (isWindows()) {
952 fs.chmodSync(file, 666); // Windows needs this unless joyent/node#3006 is resolved..
953 }
954
955 fs.unlinkSync(file);
956 } else {
957 // Assume it's a file - perhaps a try/catch belongs here?
958 if (isWindows) {
959 fs.chmodSync(file, 666); // Windows needs this unless joyent/node#3006 is resolved..
960 }
961
962 fs.unlinkSync(file);
963 }
964 }
965
966 /* Now that we know everything in the sub-tree has been deleted, we can delete the main
967 directory. Huzzah for the shopkeep. */
968 return fs.rmdirSync(_path);
969}
970
971/**
972 * return an array of arguments appending any subsequent process args
973 * @param {string} args - arguments
974 * @param {object} opts - options
975 * @returns {array[]}
976 */
977function mergeOptsToArgs(args, _opts) {
978 var argv = [].concat(process.__argv.slice(3));
979 if (argv.length) {
980 for (var c = 0; c < argv.length; c++) {
981 var arg = argv[c];
982 args.push(arg);
983 }
984 }
985 return args;
986}
987
988/**
989 * returns the proxy to use, checks:
990 * 1. proxyServer setting from config
991 * 2. environmental variables (HTTP_PROXY, HTTPS_PROXY)
992 *
993 * to set proxyServer value:
994 * appc config set proxyServer '[proxy url]'
995 *
996 * @param {object} config - config object
997 * @returns {string}
998 */
999function getProxyServer(config) {
1000 var proxy = null,
1001 parsed;
1002
1003 if (config && config.proxyServer) {
1004 parsed = urllib.parse(config.proxyServer);
1005 if (/^https?:$/.test(parsed.protocol) && parsed.hostname && parsed.hostname !== 'null') {
1006 proxy = config.proxyServer;
1007 }
1008 }
1009
1010 return proxy
1011 || process.env.HTTP_PROXY
1012 || process.env.http_proxy
1013 || process.env.HTTPS_PROXY
1014 || process.env.https_proxy
1015 || '';
1016}
1017
1018/**
1019 * return whether or not to do SSL key validation when making https requests.
1020 *
1021 * to set stricSSL value:
1022 * appc config set strictSSL [false/true]
1023 *
1024 * @param {object} config - config object
1025 * @return {boolean|null}
1026 */
1027function getStrictSSL(config) {
1028 return config ? config.strictSSL : null;
1029}
1030
1031/**
1032 * return the path to a file containing one or multiple Certificate Authority signing certificates.
1033 *
1034 * to set cafile value:
1035 * appc config set cafile '[file path, in PEM format]'
1036 *
1037 * @param {object} config - config object
1038 * @return {string|null}
1039 */
1040function getCAfile(config) {
1041 if (config && config.cafile && fs.existsSync(config.cafile)) {
1042 return config.cafile;
1043 }
1044
1045 return null;
1046}
1047
1048/**
1049 * write out the process.versions info stored when installing package
1050 *
1051 * @param {string} pkgDir - package directory
1052 */
1053function writeVersions(pkgDir) {
1054 var versionsFile = path.join(pkgDir, '.nodeversions'),
1055 versions = process.versions,
1056 versionsStr = JSON.stringify(versions);
1057
1058 debug('writing node version: %s to %s', versionsStr, versionsFile);
1059 if (fs.existsSync(versionsFile)) {
1060 fs.unlinkSync(versionsFile);
1061 }
1062 fs.writeFileSync(versionsFile, versionsStr);
1063
1064 // remove old file
1065 var oldVersionFile = path.join(pkgDir, '.nodeversion');
1066 if (fs.existsSync(oldVersionFile)) {
1067 fs.unlinkSync(oldVersionFile);
1068 }
1069}
1070
1071/**
1072 * return the process.versions info when install the package
1073 *
1074 * @param {string} installBin - install binary location
1075 * @returns {object}
1076 */
1077function readVersions(installBin) {
1078 var versionFile = path.join(installBin, '..', '..', '..', '.nodeversions'),
1079 versions;
1080
1081 if (fs.existsSync(versionFile)) {
1082 try {
1083 versions = JSON.parse(fs.readFileSync(versionFile));
1084 } catch (e) {
1085 debug('unable to read versions file.');
1086 }
1087 return versions;
1088 }
1089}
1090
1091/**
1092 * check if the minor/major NodeJS version changed since the package was installed
1093 *
1094 * @param {string} installBin - path to install binary
1095 * @return {boolean}
1096 */
1097function isNodeVersionChanged(installBin) {
1098 var version = getPackageNodeVersion(installBin),
1099 usedNode = version && version.split('.'),
1100 currentNode = process.version.split('.'),
1101 result = false;
1102
1103 if (usedNode && usedNode.length >= 2 && currentNode.length >= 2) {
1104 result = !(usedNode[0] === currentNode[0] && usedNode[1] === currentNode[1]);
1105 }
1106
1107 debug('node used %s, current version %s, result: %s', usedNode, currentNode, result);
1108 return result;
1109}
1110
1111/**
1112 * check if the modules version changed since the package was installed
1113 *
1114 * @param {string} installBin - path to install binary
1115 * @return {boolean}
1116 */
1117function isModuleVersionChanged(installBin) {
1118 var versions = readVersions(installBin),
1119 usedVersion = versions && versions.modules,
1120 currentModuleVersion = process.versions && process.versions.modules,
1121 result = false;
1122
1123 if (usedVersion && currentModuleVersion) {
1124 result = (usedVersion !== currentModuleVersion);
1125 debug('modules version used %s, current version %s, result: %s', usedVersion, currentModuleVersion, result);
1126 } else {
1127 result = isNodeVersionChanged(installBin);
1128 }
1129
1130 return result;
1131}
1132
1133/**
1134 * return the NodeJS version used to install the package
1135 *
1136 * @param {string} installBin - path to install binary
1137 * @return {string}
1138 */
1139function getPackageNodeVersion(installBin) {
1140 var versions = readVersions(installBin),
1141 usedNodeVersion = versions && versions.node;
1142
1143 if (usedNodeVersion) {
1144 return usedNodeVersion;
1145 }
1146
1147 var versionFile = path.join(installBin, '..', '..', '..', '.nodeversion');
1148 if (fs.existsSync(versionFile)) {
1149 return fs.readFileSync(versionFile).toString().trim();
1150 }
1151}
1152
1153function outputInfo(msg, isJSON) {
1154 if (isJSON) {
1155 return;
1156 }
1157
1158 exports.stdout.write(msg);
1159}
1160
1161function killDaemon(version, installBin) {
1162 var pkgFile = path.join(getInstallDir(), version, 'package', 'package.json');
1163 var pkg = fs.existsSync(pkgFile) && require(pkgFile);
1164 if (isWindows()) {
1165 installBin = '"' + process.execPath + '" "' + installBin + '"';
1166 }
1167 if (pkg && 'appcd' in pkg.dependencies) {
1168 debug('stop appcd');
1169 try {
1170 execSync(installBin + ' appcd restart');
1171 } catch (error) {
1172 // ignore
1173 debug('error killing the daemon');
1174 debug(error);
1175 }
1176 }
1177}
1178
1179function checkNodeVersion (supportedNodeRange) {
1180 if (!semver.satisfies(process.version, supportedNodeRange)) {
1181 console.log(chalk.cyan('Appcelerator Command-Line Interface'));
1182 console.log('Copyright (c) 2014-' + (new Date().getFullYear()) + ', Appcelerator, Inc. All Rights Reserved.');
1183 console.log('');
1184 console.log(chalk.red('ERROR: appc requires Node.js ' + semver.validRange(supportedNodeRange)));
1185 console.log('Visit ' + chalk.cyan('http://nodejs.org/') + ' to download a newer version.');
1186 console.log('');
1187
1188 exports.exit(1);
1189 }
1190}
1191
1192exports.checkNodeVersion = checkNodeVersion;
1193exports.getAppcDir = getAppcDir;
1194exports.getHomeDir = getHomeDir;
1195exports.getCacheDir = getCacheDir;
1196exports.getConfigFile = getConfigFile;
1197exports.getNpmCacheDirectory = getNpmCacheDirectory;
1198exports.ensureDir = ensureDir;
1199exports.expandPath = expandPath;
1200exports.getInstallDir = getInstallDir;
1201exports.listVersions = listVersions;
1202exports.getVersionJson = getVersionJson;
1203exports.getInstalledVersions = getInstalledVersions;
1204exports.getInstallBinary = getInstallBinary;
1205exports.fail = fail;
1206exports.parseOpts = parseOpts;
1207exports.parseArgs = parseArgs;
1208exports.writeVersion = writeVersion;
1209exports.getActiveVersion = getActiveVersion;
1210exports.makeURL = makeURL;
1211exports.request = request;
1212exports.requestJSON = requestJSON;
1213exports.pad = pad;
1214exports.waitMessage = waitMessage;
1215exports.okMessage = okMessage;
1216exports.infoMessage = infoMessage;
1217exports.stopSpinner = stopSpinner;
1218exports.startSpinner = startSpinner;
1219exports.canWriteDir = canWriteDir;
1220exports.checkDirectory = checkDirectory;
1221exports.abortMessage = abortMessage;
1222exports.updateCheck = updateCheck;
1223exports.isWindows = isWindows;
1224exports.resetLine = resetLine;
1225exports.rmdirSyncRecursive = rmdirSyncRecursive;
1226exports.getRequest = getRequest;
1227exports.mergeOptsToArgs = mergeOptsToArgs;
1228exports.getInstallTag = getInstallTag;
1229exports.getProxyServer = getProxyServer;
1230exports.getStrictSSL = getStrictSSL;
1231exports.getCAfile = getCAfile;
1232exports.readConfig = readConfig;
1233exports.writeVersions = writeVersions;
1234exports.isModuleVersionChanged = isModuleVersionChanged;
1235exports.getPackageNodeVersion = getPackageNodeVersion;
1236exports.outputInfo = outputInfo;
1237exports.killDaemon = killDaemon;