text: var fs = require('fs');
  var path = require('path');
  var async = require('async');
  var debug = require('debug')('node-inspector:ScriptFileStorage');
  
  var MODULE_HEADER = '(function (exports, require, module, __filename, __dirname) { ';
  var MODULE_TRAILER = '\n});';
  var MODULE_WRAP_REGEX = new RegExp(
    '^' + escapeRegex(MODULE_HEADER) +
      '([\\s\\S]*)' +
      escapeRegex(MODULE_TRAILER) + '$'
  );
  
  var CONVENTIONAL_DIRS_PATTERN = [
    /^[^\/]*\.js$/,
    /^lib\/[\s\S]*\.js$/,
    /^test\/[\s\S]*\.js$/,
    /^node_modules\/[\s\S]*\.js$/
  ];
  var ALL_JS_PATTERN = [
    /^[\s\S]*\.js$/
  ];
  
  function escapeRegex(str) {
    return str.replace(/([/\\.?*()^${}|[\]])/g, '\\$1');
  }
  
  /**
   * @param {{preload}} config
   * @param {ScriptManager} scriptManager
   * @constructor
   */
  function ScriptFileStorage(config, session) {
    config = config || {};
    this._hidden = config.hidden;
    this._scriptManager = session.scriptManager;
    this._noPreload = config.preload === false;
  }
  
  var $class = ScriptFileStorage.prototype;
  
  $class.save = function(path, content, callback) {
    var match = MODULE_WRAP_REGEX.exec(content);
    if (!match) {
      callback(new Error('The new content is not a valid node.js script.'));
      return;
    }
    var newSource = match[1];
    async.waterfall([
      fs.readFile.bind(fs, path, 'utf-8'),
  
      function(oldContent, cb) {
        var match = /^(\#\!.*)/.exec(oldContent);
        if (match)
          newSource = match[1] + newSource;
  
        fs.writeFile(path, newSource, cb);
      }
    ],
    callback);
  };
  
  /**
   * @param {string} path
   * @param {function(Object, string)} callback
   */
  $class.load = function(path, callback) {
    var scriptId = this._scriptManager.findScriptIdByPath(path);
    // If requested source was loaded in app (scriptId != null), we can't expect that it is equal
    // to the file with requested name stored in fs.
    // So, if requested source was loaded in app, we need to require it from app.
    if (scriptId != null) {
      this._scriptManager.getScriptSourceById(scriptId, callback);
    } else {
      fs.readFile(
        path,
        'utf-8',
        function(err, content) {
          if (err) return callback(err);
  
          // remove shebang
          // content = content.replace(/^\#\!.*/, '');
  
          // var source = MODULE_HEADER + content + MODULE_TRAILER;
          return callback(null, content);
        }
      );
    }
  };
  
  /**
   * @param {string} mainScriptFile
   * @param {function(Object, string)} callback
   * @this {ScriptFileStorage}
   */
  $class.findApplicationRoot = function(mainScriptFile, callback) {
    fs.realpath(mainScriptFile, function(err, realPath) {
      if (err) {
        console.log('Cannot resolve real path of %s: %s', mainScriptFile, err);
        realPath = mainScriptFile;
      }
      this._findApplicationRootForRealFile(realPath, callback);
    }.bind(this));
  };
  
  /**
   * For a given script file, find the root directory containing all application
   * source files.
   *
   * Example:
   *   file = ~/work/app/bin/cli.js
   *   root = ~/work/app
   *
   * The algorithm:
   *
   * By default, we assume that the source file is in the root directory
   * (~/work/app/bin in the example above).
   *
   * If this directory does not contain 'package.json' and the parent directory
   * contains 'package.json', then we assume the parent directory is
   * the application root (~/work/app in the example above).
   *
   * @param {string} file
   * @param {function(Object, string)} callback
   * @this {ScriptFileStorage}
   */
  $class._findApplicationRootForRealFile = function(file, callback) {
    var mainDir = path.dirname(file);
    var parentDir = path.dirname(mainDir);
  
    async.detect(
      [mainDir, parentDir],
      this._isApplicationRoot.bind(this),
      function(result) {
        callback(null, result || mainDir, !!result);
      }
    );
  };
  
  /**
   * @param {string} folder
   * @param {function(boolean)} callback
   */
  $class._isApplicationRoot = function(folder, callback) {
    fs.exists(path.join(folder, 'package.json'), callback);
  };
  
  /**
   * @param {string} rootFolder
   * @param {string} include
   * @param {function(Object, Array.<string>?)} callback
   */
  $class.listScripts = function(rootFolder, include, callback) {
    // This simpler solution unfortunately does not work on windows
    // see https://github.com/isaacs/node-glob/pull/68
    // glob(
    //   '**/*.js',
    //   { root: rootFolder },
    //    callback
    // );
  
    debug('glob %s on %s', include, rootFolder);
  
    var exclude = this._hidden;
    list(rootFolder, include, exclude, function(error, result) {
      if (result) {
        result = result.map(function(unixPath) {
          return unixPath.split('/').join(path.sep);
        });
      }
  
      debug('glob returned %s files', error || result.length);
      callback(error, result);
    });
  };
  
  $class._findScriptsOfRunningApp = function(mainScriptFile, callback) {
    if (!mainScriptFile) {
      // mainScriptFile is null when running in the REPL mode
      return process.nextTick(callback.bind(null, null, []));
    }
  
    async.waterfall(
      [
        this.findApplicationRoot.bind(this, mainScriptFile),
        function(dir, isRoot, cb) {
          var include = isRoot ? ALL_JS_PATTERN : CONVENTIONAL_DIRS_PATTERN;
          this.listScripts(dir, include, cb);
        }.bind(this)
      ],
      callback
    );
  };
  
  $class._findScriptsOfStartDirectoryApp = function(startDirectory, callback) {
    this._isApplicationRoot(
      startDirectory,
      function handleIsStartDirectoryApplicationRoot(result) {
        if (!result) {
          callback(null, []);
        } else {
          this.listScripts(startDirectory, ALL_JS_PATTERN, callback);
        }
      }.bind(this)
    );
  };
  
  /**
   * @param {string} startDirectory
   * @param {string} mainScriptFile
   * @param {function(Object, Array.<string>)} callback
   * @this {ScriptFileStorage}
   */
  $class.findAllApplicationScripts = function(startDirectory, mainScriptFile, callback) {
    if (this._noPreload) {
      return process.nextTick(function() { callback(null, []); });
    }
    async.series(
      [
        this._findScriptsOfRunningApp.bind(this, mainScriptFile),
        this._findScriptsOfStartDirectoryApp.bind(this, startDirectory)
      ],
      function(err, results) {
        if (err) return callback(err);
  
        var files = results[0].concat(results[1]);
  
        debug('findAllApplicationScripts returned %s files', files.length);
        return callback(null, files);
      }.bind(this)
    );
  };
  
  var list = function(root, include, exclude, cb) {
    include = include || [];
    exclude = exclude || [];
  
    var folders = [root];
    var cache = {};
    var list = [];
    var node = null;
    var realpaths = {};
  
    iterator();
  
    function iterator(error) {
      if (error) console.warn('Access to ' + node + ' from ResourceTree denied. (' + error + ')');
      if (folders.length) return iterate(node = folders.pop());
      return cb(null, list);
    }
  
    function iterate(node) {
      async.waterfall([
        fs.readdir.bind(fs, node),
        function(paths, cb) {
          async.each(paths, function(child, cb) {
            var apath = path.join(node, child);
            var rpath = relative(apath);
  
            fs.realpath(apath, realpaths, function(err, realpath) {
              if (err) { return cb(err); }
              if (cache[realpath] || excluded(rpath)) return cb();
              cache[realpath] = true;
  
              fs.stat(realpath, function(error, pstat) {
                if (error) return cb(error);
  
                if (pstat.isDirectory()) folders.push(realpath);
                if (pstat.isFile() && included(rpath)) list.push(realpath);
                cb();
              });
            });
          }, cb);
        }
      ], iterator);
    }
  
    function included(rpath) {
      return include.some(function(rx) { return rx.test(rpath); });
    }
  
    function excluded(rpath) {
      return exclude.some(function(rx) { return rx.test(rpath); });
    }
  
    function relative(apath) {
      return path.relative(root, apath);
    }
  };
  
  exports.ScriptFileStorage = ScriptFileStorage;
  