diff --git a/.gitignore b/.gitignore
index b5c98c948d9c2eb75df122268b315b29f15a9e93..3bdc4e066866d0433aafc3341fa4b43b0b420b58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,6 @@ include/ost-config.php
 .DS_Store
 .vagrant
 Vagrantfile
+
+# Staging directory used for packaging script
+stage
diff --git a/setup/cleanup-codebase.sh b/setup/scripts/cleanup-codebase.sh
similarity index 100%
rename from setup/cleanup-codebase.sh
rename to setup/scripts/cleanup-codebase.sh
diff --git a/setup/scripts/manage.php b/setup/scripts/manage.php
new file mode 100755
index 0000000000000000000000000000000000000000..c738318291c9fb861bad7139f7fd75403da6d876
--- /dev/null
+++ b/setup/scripts/manage.php
@@ -0,0 +1,60 @@
+#!/usr/bin/env php
+<?php
+
+require_once "modules/class.module.php";
+
+class Manager extends Module {
+    var $prologue =
+        "Manage one or more osTicket installations";
+
+    var $arguments = array(
+        'action' => "Action to be managed"
+    );
+
+    var $usage = '$script action [options] [arguments]';
+
+    var $autohelp = false;
+
+    function showHelp() {
+        foreach (glob(dirname(__file__).'/modules/*.php') as $script)
+            include_once $script;
+
+        global $registered_modules;
+        $this->epilog = 
+            "Currently available modules follow. Use 'manage.php <module>
+            --help' for usage regarding each respective module:";
+
+        parent::showHelp();
+        
+        echo "\n";
+        foreach ($registered_modules as $name=>$mod)
+            echo str_pad($name, 20) . $mod->prologue . "\n";
+    }
+
+    function run() {
+        if ($this->getOption('help') && !$this->getArgument('action'))
+            $this->showHelp();
+
+        else {
+            $action = $this->getArgument('action');
+
+            global $argv;
+            foreach ($argv as $idx=>$val)
+                if ($val == $action)
+                    unset($argv[$idx]);
+
+            include_once dirname(__file__) . '/modules/' . $action . '.php';
+            $module = Module::getInstance($action);
+            $module->run();
+        }
+    }
+}
+
+if (php_sapi_name() != "cli")
+    die("Management only supported from command-line\n");
+
+$manager = new Manager();
+$manager->parseOptions();
+$manager->run();
+
+?>
diff --git a/setup/scripts/modules/class.module.php b/setup/scripts/modules/class.module.php
new file mode 100644
index 0000000000000000000000000000000000000000..de30f16321457bec2bb0c8762ed790d583e3ca6e
--- /dev/null
+++ b/setup/scripts/modules/class.module.php
@@ -0,0 +1,214 @@
+<?php
+
+class Option {
+    
+    var $default = false;
+
+    function Option() {
+        call_user_func_array(array($this, "__construct"), func_get_args());
+    }
+
+    function __construct($options=false) {
+        list($this->short, $this->long) = array_slice($options, 0, 2);
+        $this->help = (isset($options['help'])) ? $options['help'] : "";
+        $this->action = (isset($options['action'])) ? $options['action']
+            : "store";
+        $this->dest = (isset($options['dest'])) ? $options['dest']
+            : substr($this->long, 2);
+        $this->type = (isset($options['type'])) ? $options['type']
+            : 'string';
+        $this->const = (isset($options['const'])) ? $options['const']
+            : null;
+        $this->default = (isset($options['default'])) ? $options['default']
+            : null;
+        $this->metavar = (isset($options['metavar'])) ? $options['metavar']
+            : 'var';
+    }
+
+    function hasArg() {
+        return $this->action != 'store_true'
+            && $this->action != 'store_false';
+    }
+
+    function handleValue(&$destination, $args) {
+        $nargs = 0;
+        $value = array_shift($args);
+        if ($value[0] == '-')
+            $value = null;
+        elseif ($value)
+            $nargs = 1;
+        switch ($this->action) {
+            case 'store_true':
+                $value = true;
+                break;
+            case 'store_false':
+                $value = false;
+                break;
+            case 'store_const':
+                $value = $this->const;
+                break;
+            case 'store':
+            default:
+                if ($this->type == 'int')
+                    $value = (int)$value;
+                break;
+        }
+        $destination[$this->dest] = $value;
+        return $nargs;
+    }
+
+    function toString() {
+        $short = explode(':', $this->short);
+        $long = explode(':', $this->long);
+        if ($this->nargs == '?')
+            $switches = sprintf('    %s [%3$s], %s[=%3$s]', $short[0],
+                $long[0], $this->metavar);
+        elseif ($this->hasArg())
+            $switches = sprintf('    %s %3$s, %s=%3$s', $short[0], $long[0],
+                $this->metavar);
+        else
+            $switches = sprintf("    %s, %s", $short[0], $long[0]);
+        $help = preg_replace('/\s+/', ' ', $this->help);
+        if (strlen($switches) > 24)
+            $help = "\n" . str_repeat(" ", 24) . $help;
+        else
+            $switches = str_pad($switches, 24);
+        $help = wordwrap($help, 54, "\n" . str_repeat(" ", 24));
+        return $switches . $help;
+    }
+}
+
+class Module {
+
+    var $options = array();
+    var $arguments = array();
+    var $prologue = "";
+    var $epilog = "";
+    var $usage = '$script [options] $args [arguments]';
+    var $autohelp = true;
+
+    function Module() {
+        call_user_func_array(array($this, '__construct'), func_get_args());
+    }
+
+    function __construct() {
+        $this->options['help'] = array("-h","--help",
+            'action'=>'store_true',
+            'help'=>"Display this help message");
+        foreach ($this->options as &$opt)
+            $opt = new Option($opt);
+    }
+
+    function showHelp() {
+        if ($this->prologue)
+            echo $this->prologue . "\n\n";
+
+        echo "Usage:\n";
+        global $argv;
+        echo "    " . str_replace(
+                array('$script', '$args'),
+                array($argv[0], implode(' ', array_keys($this->arguments))),
+            $this->usage) . "\n";
+
+        ksort($this->options);
+        if ($this->options) {
+            echo "\nOptions:\n";
+            foreach ($this->options as $name=>$opt)
+                echo $opt->toString() . "\n";
+        }
+
+        if ($this->arguments) {
+            echo "\nArguments:\n";
+            foreach ($this->arguments as $name=>$help)
+                echo $name . "\n    " . wordwrap(
+                    preg_replace('/\s+/', ' ', $help), 76, "\n    ");
+        }
+
+        if ($this->epilog) {
+            echo "\n\n";
+            $epilog = preg_replace('/\s+/', ' ', $this->epilog);
+            echo wordwrap($epilog, 76, "\n");
+        }
+
+        echo "\n";
+    }
+
+    function getOption($name, $default=false) {
+        $this->parseOptions();
+        if (isset($this->_options[$name]))
+            return $this->_options[$name];
+        else
+            return $default;
+    }
+
+    function getArgument($name, $default=false) {
+        $this->parseOptions();
+        foreach (array_keys($this->arguments) as $idx=>$arg)
+            if ($arg == $name && isset($this->_args[$idx]))
+                return $this->_args[$idx];
+        return $default;
+    }
+
+    function parseOptions() {
+        if (is_array($this->_options))
+            return;
+
+        global $argv;
+        list($this->_options, $this->_args) =
+            $this->parseArgs(array_slice($argv, 1));
+
+        foreach (array_keys($this->arguments) as $idx=>$name)
+            if (!isset($this->_args[$idx]))
+                $this->optionError($name . " is a required argument");
+
+        if ($this->autohelp && $this->getOption('help')) {
+            $this->showHelp();
+            die();
+        }
+    }
+
+    function optionError($error) {
+        echo "Error: " . $error . "\n\n";
+        $this->showHelp();
+        die();
+    }
+
+    /* abstract */ function run() {
+    }
+
+    /* static */ function register($action, $class) {
+        global $registered_modules;
+        $registered_modules[$action] = new $class();
+    }
+
+    /* static */ function getInstance($action) {
+        global $registered_modules;
+        return $registered_modules[$action];
+    }
+
+    function parseArgs($argv) {
+        $options = $args = array();
+        $argv = array_slice($argv, 0);
+        while ($arg = array_shift($argv)) {
+            if (strpos($arg, '=') !== false) {
+                list($arg, $value) = explode('=', $arg, 2);
+                array_unshift($argv, $value);
+            }
+            $found = false;
+            foreach ($this->options as $opt) {
+                if ($opt->short == $arg || $opt->long == $arg) {
+                    if ($opt->handleValue($options, $argv))
+                        array_shift($argv);
+                    $found = true;
+                }
+            }
+            if (!$found && $arg[0] != '-')
+                $args[] = $arg;
+        }
+        return array($options, $args);
+    }
+}
+
+$registered_modules = array();
+
+?>
diff --git a/setup/scripts/modules/unpack.php b/setup/scripts/modules/unpack.php
new file mode 100644
index 0000000000000000000000000000000000000000..0f6aed8ab9cc6ac862e09389393c57f3e62a7fb8
--- /dev/null
+++ b/setup/scripts/modules/unpack.php
@@ -0,0 +1,158 @@
+<?php
+
+require_once dirname(__file__) . "/class.module.php";
+
+class Unpacker extends Module {
+    
+    var $prologue = "Unpacks osTicket into target install path";
+
+    var $epilog =
+        "Copies an unpacked osticket tarball or zipfile into a production
+         location, optionally placing the include/ folder in a separate 
+         location if requested";
+
+    var $options = array(
+        'include' => array('-i','--include', 'metavar'=>'path', 'help'=>
+            "The include/ folder, which contains the bulk of osTicket's source
+             code can be located outside of the install path. This is recommended
+             for better security. If you would like to install the include/
+             folder somewhere else, give the path here. Note that the full
+             path is assumed, so give path/to/include/ to unpack the source
+             code in that folder. The folder will be automatically created if
+             it doesn't already exist."
+        ),
+    );
+
+    var $arguments = array(
+        'install-path' =>
+            "The destination for osTicket to reside. Use the --include
+             option to specify destination of the include/ folder, if the
+             administrator should chose to locate it separate from the
+             main installation path.",
+    );
+
+    function find_upload_folder() {
+        # Hop up to the root folder
+        $start = dirname(__file__);
+        for (;;) {
+            if (is_dir($start . '/upload')) break;
+            $start .= '/..';
+        }
+        return realpath($start.'/upload');
+    }
+
+    function change_include_dir($include_path) {
+        # Read the main.inc.php script
+        $main_inc_php = $this->destination . '/main.inc.php';
+        $lines = explode("\n", file_get_contents($main_inc_php));
+        # Try and use ROOT_PATH
+        if (strpos($include_path, $this->destination) === 0)
+            $include_path = "ROOT_PATH . '" .
+                str_replace($this->destination, '', $include_path) . "'";
+        else
+            $include_path = "'$include_path'";
+        # Find the line that defines INCLUDE_DIR
+        foreach ($lines as &$line) {
+            if (preg_match("/(\s*)define\s*\(\s*'INCLUDE_DIR'/", $line, $match)) {
+                # Replace the definition with the new locatin
+                $line = $match[1] . "define('INCLUDE_DIR', "
+                    . $include_path
+                    . "); // Set by installer";
+                break;
+            }
+        }
+        if (!file_put_contents($main_inc_php, implode("\n", $lines)))
+            die("Unable to configure location of INCLUDE_DIR in main.inc.php\n");
+    }
+
+    function exclude($pattern, $match) {
+        if (!$pattern) {
+            return false;
+        } elseif (is_array($pattern)) {
+            foreach ($pattern as $p)
+                if (fnmatch($p, $match))
+                    return true;
+        } else {
+            return fnmatch($pattern, $match);
+        }
+        return false;
+    }
+
+    function unpackage($folder, $destination, $recurse=true, $exclude=false) {
+        foreach (glob($folder, GLOB_BRACE|GLOB_NOSORT) as $file) {
+            if ($this->exclude($exclude, $file))
+                continue;
+            if (is_file($file)) {
+                if (!is_dir($destination))
+                    mkdir($destination, 0751, true);
+                copy($file, $destination . '/' . basename($file));
+            }
+        }
+        if ($recurse) {
+            foreach (glob(dirname($folder).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
+                if ($this->exclude($exclude, $dir))
+                    continue;
+                $this->unpackage(
+                    dirname($folder).'/'.basename($dir).'/'.basename($folder),
+                    $destination.'/'.basename($dir),
+                    $recurse - 1, $exclude);
+            }
+        }
+    }
+
+    function get_include_dir() {
+        $main_inc_php = $this->destination . '/main.inc.php';
+        $lines = preg_grep("/define\s*\(\s*'INCLUDE_DIR'/",
+            explode("\n", file_get_contents($main_inc_php)));
+
+        // NOTE: that this won't work for crafty folks who have a define or some
+        //       variable in the value of their include path
+        if (!defined('ROOT_DIR')) define('ROOT_DIR', $this->destination . '/');
+        foreach ($lines as $line)
+            eval($line);
+
+        return INCLUDE_DIR;
+    }
+
+    function run() {
+        $this->destination = $this->getArgument('install-path');
+        if (!is_dir($this->destination))
+            if (!mkdir($this->destination, 0751, true))
+                $this->die("Destination path does not exist and cannot be created");
+
+        # Determine if this is an upgrade, and if so, where the include/
+        # folder is currently located
+        $upgrade = file_exists("{$this->destination}/main.inc.php");
+
+        # Locate the upload folder
+        $upload = $this->find_upload_folder();
+
+        # Unpack the upload folder to the destination, except the include folder
+        if ($upgrade)
+            # Get the current value of the INCLUDE_DIR before overwriting
+            # main.inc.php
+            $include = $this->get_include_dir();
+        $this->unpackage("$upload/*", $this->destination, -1, "*include");
+
+        if (!$upgrade) {
+            if ($this->getOption('include')) {
+                $location = $this->getOption('include');
+                if (!is_dir("$location/"))
+                    if (!mkdir("$location/", 0751, true))
+                        die("Unable to create folder for include/ files\n");
+                $this->unpackage("$upload/include/*", $location, -1);
+                $this->change_include_dir($location);
+            }
+            else
+                $this->unpackage("$upload/include/*", "{$this->destination}/include", -1);
+        }
+        else {
+            $this->unpackage("$upload/include/*", $include, -1);
+            # Change the new main.inc.php to reflect the location of the
+            # include/ directory
+            $this->change_include_dir($include);
+        }
+    }
+}
+
+Module::register('unpack', 'Unpacker');
diff --git a/setup/scripts/package.php b/setup/scripts/package.php
new file mode 100755
index 0000000000000000000000000000000000000000..22542b6e0fe9d7dc944af1839c91b473047e4e9d
--- /dev/null
+++ b/setup/scripts/package.php
@@ -0,0 +1,131 @@
+#!/usr/bin/env php
+<?php
+
+if (php_sapi_name() != 'cli')
+    die("Only command-line packaging is supported");
+
+$stage_folder = "stage";
+$stage_path = dirname(__file__) . '/' . $stage_folder;
+
+function get_osticket_root_path() {
+    # Hop up to the root folder
+    $start = dirname(__file__);
+    for (;;) {
+        if (file_exists($start . '/main.inc.php')) break;
+        $start .= '/..';
+    }
+    return realpath($start);
+}
+
+# Check PHP syntax across all php files
+function glob_recursive($pattern, $flags = 0) {
+    $files = glob($pattern, $flags);
+    foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
+        $files = array_merge($files, 
+            glob_recursive($dir.'/'.basename($pattern), $flags));
+    }
+    return $files;
+}
+
+$root = get_osticket_root_path();
+
+function exclude($pattern, $match) {
+    if (is_array($pattern)) {
+        foreach ($pattern as $p)
+            if (fnmatch($p, $match))
+                return true;
+    }
+    else
+        return fnmatch($pattern, $match);
+    return false;
+}
+
+function package($pattern, $destination, $recurse=false, $exclude=false) {
+    global $root, $stage_path;
+    $search = $root . '/' . $pattern;
+    echo "Packaging " . $search . "\n";
+    foreach (glob($search, GLOB_BRACE|GLOB_NOSORT) as $file) {
+        if (is_file($file)) {
+            if ($exclude && exclude($exclude, $file))
+                continue;
+            if (!is_dir("$stage_path/$destination"))
+                mkdir("$stage_path/$destination", 0777, true);
+            copy($file, $stage_path . '/' . $destination . '/' . basename($file));
+        }
+    }
+    if ($recurse) {
+        foreach (glob(dirname($search).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
+            if ($exclude && exclude($exclude, $dir))
+                continue;
+            package(dirname($pattern).'/'.basename($dir).'/'.basename($pattern),
+                $destination.'/'.basename($dir),
+                $recurse-1, $exclude);
+        }
+    }
+}
+
+# Create the stage folder for the install files
+if (!is_dir($stage_path))
+    mkdir($stage_path);
+else {
+    $dirs = array();
+    foreach (glob_recursive($stage_path . '/*') as $file)
+        if (is_dir($file))
+            $dirs[] = $file;
+        else
+            unlink($file);
+    sort($dirs);
+    foreach (array_reverse($dirs) as $dir)
+        rmdir($dir);
+}
+
+# Source code goes into 'upload'
+mkdir($stage_path . '/upload');
+
+# Load the root directory files
+package("*.php", 'upload/');
+
+# Load the client interface
+foreach (array('assets','css','images','js') as $dir)
+    package("$dir/*", "upload/$dir", -1, "*less");
+
+# Load the knowledgebase
+package("kb/*.php", "upload/kb");
+
+# Load the staff interface
+package("scp/*.php", "upload/scp/", -1);
+foreach (array('css','images','js') as $dir)
+    package("$dir/*", "upload/scp/$dir", -1);
+
+# Load in the scripts
+mkdir("$stage_path/scripts/");
+package("setup/scripts/*", "scripts/", -1, "*stage");
+
+# Load the heart of the system
+package("include/*.php", "upload/include", -1);
+# And the sql patches
+package("include/upgrader/*.sql", "upload/include/upgrader", -1);
+
+# Include the installer
+package("setup/*.{php,txt}", "upload/setup", -1, array("*scripts","*test","*stage"));
+foreach (array('css','images','js') as $dir)
+    package("setup/$dir/*", "upload/setup/$dir", -1);
+package("setup/inc/sql/*.{sql,md5}", "upload/setup/inc/sql", -1);
+
+# Load the license and documentation
+package("*.{txt,md}", "");
+
+# Make an archive of the stage folder
+$version_info = preg_grep('/THIS_VERSION/',
+    explode("\n", file_get_contents("$root/main.inc.php")));
+
+foreach ($version_info as $line)
+    eval($line);
+
+$pwd = getcwd();
+chdir($stage_path);
+shell_exec("tar cjf '$pwd/osticket-".THIS_VERSION.".tar.bz2' *");
+shell_exec("zip -r '$pwd/osticket-".THIS_VERSION.".zip' *");
+
+chdir($pwd);
+?>