diff --git a/app/__init__.py b/app/__init__.py
index de8afb159befdc88b472ba8233ba0d91f8697749..3afa99afba64b70a21216848880f7d9a9020e3e1 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -60,6 +60,9 @@ def create_app(config: Config = Config) -> Flask:
     from .errors import init_app as init_error_handlers
     init_error_handlers(app)
 
+    from .cli import init_app as init_cli
+    init_cli(app)
+
     from .admin import bp as admin_blueprint
     default_breadcrumb_root(admin_blueprint, '.admin')
     app.register_blueprint(admin_blueprint, url_prefix='/admin')
@@ -69,7 +72,7 @@ def create_app(config: Config = Config) -> Flask:
 
     from .auth import bp as auth_blueprint
     default_breadcrumb_root(auth_blueprint, '.')
-    app.register_blueprint(auth_blueprint, url_prefix='/')
+    app.register_blueprint(auth_blueprint)
 
     from .contributions import bp as contributions_blueprint
     default_breadcrumb_root(contributions_blueprint, '.contributions')
@@ -85,7 +88,7 @@ def create_app(config: Config = Config) -> Flask:
 
     from .main import bp as main_blueprint
     default_breadcrumb_root(main_blueprint, '.')
-    app.register_blueprint(main_blueprint, url_prefix='/')
+    app.register_blueprint(main_blueprint)
 
     from .services import bp as services_blueprint
     default_breadcrumb_root(services_blueprint, '.services')
diff --git a/app/cli.py b/app/cli.py
deleted file mode 100644
index e59a080d97050eb84aeb431efe7f5e2e0bdeae79..0000000000000000000000000000000000000000
--- a/app/cli.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from flask import current_app
-from flask_migrate import upgrade
-import click
-import os
-from app.models import (
-    CorpusFollowerRole,
-    Role,
-    SpaCyNLPPipelineModel,
-    TesseractOCRPipelineModel,
-    User
-)
-
-
-def _make_default_dirs():
-    base_dir = current_app.config['NOPAQUE_DATA_DIR']
-
-    default_directories = [
-        os.path.join(base_dir, 'tmp'),
-        os.path.join(base_dir, 'users')
-    ]
-    for directory in default_directories:
-        if os.path.exists(directory):
-            if not os.path.isdir(directory):
-                raise NotADirectoryError(f'{directory} is not a directory')
-        else:
-            os.mkdir(directory)
-
-
-def register(app):
-    @app.cli.command()
-    def deploy():
-        ''' Run deployment tasks. '''
-        # Make default directories
-        print('Make default directories')
-        _make_default_dirs()
-
-        # migrate database to latest revision
-        print('Migrate database to latest revision')
-        upgrade()
-
-        # Insert/Update default database values
-        print('Insert/Update default Roles')
-        Role.insert_defaults()
-        print('Insert/Update default Users')
-        User.insert_defaults()
-        print('Insert/Update default CorpusFollowerRoles')
-        CorpusFollowerRole.insert_defaults()
-        print('Insert/Update default SpaCyNLPPipelineModels')
-        SpaCyNLPPipelineModel.insert_defaults()
-        print('Insert/Update default TesseractOCRPipelineModels')
-        TesseractOCRPipelineModel.insert_defaults()
-
-    @app.cli.group()
-    def converter():
-        ''' Converter commands. '''
-        pass
-
-    @converter.command()
-    @click.argument('json_db')
-    @click.argument('data_dir')
-    def sandpaper(json_db, data_dir):
-        ''' Sandpaper converter '''
-        from app.converters.sandpaper import convert
-        convert(json_db, data_dir)
-
-    @app.cli.group()
-    def test():
-        ''' Test commands. '''
-        pass
-
-    @test.command('run')
-    def run_test():
-        ''' Run unit tests. '''
-        from unittest import TestLoader, TextTestRunner
-        from unittest.suite import TestSuite
-        tests: TestSuite = TestLoader().discover('tests')
-        TextTestRunner(verbosity=2).run(tests)
diff --git a/app/cli/__init__.py b/app/cli/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1803deeaa04051841aece25a1da9cdd39c0ee720
--- /dev/null
+++ b/app/cli/__init__.py
@@ -0,0 +1,10 @@
+from .converter import init_app as converter_init_app
+from .corpus import init_app as corpus_init_app
+from .main import init_app as main_init_app
+
+
+
+def init_app(app):
+    converter_init_app(app)
+    corpus_init_app(app)
+    main_init_app(app)
diff --git a/app/cli/converter.py b/app/cli/converter.py
new file mode 100644
index 0000000000000000000000000000000000000000..4d07bc3039ca27341d174641fc6ab83e3ebc4ee7
--- /dev/null
+++ b/app/cli/converter.py
@@ -0,0 +1,21 @@
+import click
+
+
+def init_app(app):
+    @app.cli.group('converter')
+    def converter():
+        ''' Converter commands. '''
+        pass
+
+    @converter.group('sandpaper')
+    def sandpaper_converter():
+        ''' Sandpaper converter commands. '''
+        pass
+
+    @sandpaper_converter.command('run')
+    @click.argument('json_db')
+    @click.argument('data_dir')
+    def run_sandpaper_converter(json_db, data_dir):
+        ''' Run the sandpaper converter. '''
+        from app.converters.sandpaper import convert
+        convert(json_db, data_dir)
diff --git a/app/cli/corpus.py b/app/cli/corpus.py
new file mode 100644
index 0000000000000000000000000000000000000000..e79269f0691894edb133fcda2258a9999a676966
--- /dev/null
+++ b/app/cli/corpus.py
@@ -0,0 +1,23 @@
+from app.models import Corpus, CorpusStatus
+
+
+def init_app(app):
+    @app.cli.group('corpus')
+    def corpus():
+        ''' Corpus commands. '''
+        pass
+
+    @corpus.command('dismantle')
+    def dismantle():
+        ''' Dismantle built corpora. '''
+        status = [
+            CorpusStatus.QUEUED,
+            CorpusStatus.BUILDING,
+            CorpusStatus.BUILT,
+            CorpusStatus.STARTING_ANALYSIS_SESSION,
+            CorpusStatus.RUNNING_ANALYSIS_SESSION,
+            CorpusStatus.CANCELING_ANALYSIS_SESSION
+        ]
+        for corpus in [x for x in Corpus.query.all() if x.status in status]:
+            corpus.status = CorpusStatus.SUBMITTED
+            corpus.num_analysis_sessions = 0
diff --git a/app/cli/main.py b/app/cli/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..2022d6090b2818d0d2d5be278b42e9d87feb0238
--- /dev/null
+++ b/app/cli/main.py
@@ -0,0 +1,45 @@
+from flask import current_app
+from flask_migrate import upgrade
+import os
+from app.models import (
+    CorpusFollowerRole,
+    Role,
+    SpaCyNLPPipelineModel,
+    TesseractOCRPipelineModel,
+    User
+)
+
+
+def init_app(app):
+    @app.cli.command('deploy')
+    def deploy():
+        ''' Run deployment tasks. '''
+        # Make default directories
+        print('Make default directories')
+        base_dir = current_app.config['NOPAQUE_DATA_DIR']
+        default_dirs = [
+            os.path.join(base_dir, 'tmp'),
+            os.path.join(base_dir, 'users')
+        ]
+        for dir in default_dirs:
+            if os.path.exists(dir):
+                if not os.path.isdir(dir):
+                    raise NotADirectoryError(f'{dir} is not a directory')
+            else:
+                os.mkdir(dir)
+
+        # migrate database to latest revision
+        print('Migrate database to latest revision')
+        upgrade()
+
+        # Insert/Update default database values
+        print('Insert/Update default Roles')
+        Role.insert_defaults()
+        print('Insert/Update default Users')
+        User.insert_defaults()
+        print('Insert/Update default CorpusFollowerRoles')
+        CorpusFollowerRole.insert_defaults()
+        print('Insert/Update default SpaCyNLPPipelineModels')
+        SpaCyNLPPipelineModel.insert_defaults()
+        print('Insert/Update default TesseractOCRPipelineModels')
+        TesseractOCRPipelineModel.insert_defaults()
diff --git a/app/main/__init__.py b/app/main/__init__.py
index 6563022425984947949a2d8c5debf6124bf31de9..f32fed5fb6e585ba1675ce66818e7b503cbb0059 100644
--- a/app/main/__init__.py
+++ b/app/main/__init__.py
@@ -1,5 +1,5 @@
 from flask import Blueprint
 
 
-bp = Blueprint('main', __name__)
+bp = Blueprint('main', __name__, cli_group=None)
 from . import routes
diff --git a/app/main/routes.py b/app/main/routes.py
index 708262db25d2fd1bb93806a7114f61cfc5032ba3..56c37116806e05d76d4804fb7cd505458292ebdc 100644
--- a/app/main/routes.py
+++ b/app/main/routes.py
@@ -7,7 +7,7 @@ from sqlalchemy import or_
 from . import bp
 
 
-@bp.route('', methods=['GET', 'POST'])
+@bp.route('/', methods=['GET', 'POST'])
 @register_breadcrumb(bp, '.', '<i class="material-icons">home</i>')
 def index():
     form = LoginForm()
diff --git a/app/tests/__init__.py b/app/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4665c05de709a739f9a95bd3d58f26f8968755ee
--- /dev/null
+++ b/app/tests/__init__.py
@@ -0,0 +1,5 @@
+from flask import Blueprint
+
+
+bp = Blueprint('tests', __name__)
+from . import cli
diff --git a/app/tests/cli.py b/app/tests/cli.py
new file mode 100644
index 0000000000000000000000000000000000000000..a620bf0bea4fb4af8e90f511de55860d5e6e1b8b
--- /dev/null
+++ b/app/tests/cli.py
@@ -0,0 +1,9 @@
+from unittest import TestLoader, TextTestRunner
+from unittest.suite import TestSuite
+from . import bp
+
+@bp.cli.command('run')
+def run_test():
+    ''' Run unit tests. '''
+    tests: TestSuite = TestLoader().discover('tests')
+    TextTestRunner(verbosity=2).run(tests)
diff --git a/nopaque.py b/nopaque.py
index b369556a2885d94be7185d609cdf0fedfa061036..bd9a91657a503913e551dc190f4a0c8326316509 100644
--- a/nopaque.py
+++ b/nopaque.py
@@ -3,7 +3,7 @@
 import eventlet
 eventlet.monkey_patch()
 
-from app import cli, create_app, db, scheduler, socketio  # noqa
+from app import create_app, db, scheduler, socketio  # noqa
 from app.models import (
     Avatar,
     Corpus,
@@ -23,7 +23,6 @@ from typing import Any, Dict  # noqa
 
 
 app: Flask = create_app()
-cli.register(app)
 
 
 @app.shell_context_processor