CLI

The project shell entry point—orchestrate all commands

The CLI module is pure glue code: parse arguments, validate flags, and dispatch to the appropriate function. No business logic lives here—just the translation layer between command-line invocations and the underlying modules.

This separation keeps the CLI thin and the core functions testable. You can call init_nbdev(), sync(), or ship() directly from Python without going through argparse, useful for testing and for potential future GUIs or APIs.

Argument Parsing

The main entry point constructs the argument parser hierarchy: a root parser with subcommands, each subcommand with its own arguments and flags.


source

main

 main ()

Main CLI entry point

CLI Architecture

Subcommand dispatch

Argparse’s subparsers creates a command hierarchy: pj <command> [options]. Each subcommand gets its own parser with specific arguments. The dest="command" attribute stores which subcommand was invoked, making dispatch trivial: check args.command and call the appropriate function.

Argument groups

The init subcommand has many flags. Organizing them into logical groups (output_group, gh_group, py_group, etc.) structures the help text. Users see related options clustered together rather than one long alphabetized list.

Raw description formatting

RawDescriptionHelpFormatter preserves whitespace and newlines in description and epilog strings. This lets us write multi-line examples and numbered workflows that display exactly as written, rather than getting reflowed into paragraphs.

Default values

Many arguments have defaults specified in the business logic functions, not here. The CLI passes None for optional arguments, and the called function provides the default. This keeps defaults in one place—changing the default license doesn’t require updating both CLI and init logic.

The dispatch pattern

The final if/elif chain is the entire dispatch logic. Parse args, check which command, call the function, pass the args object. No validation, no transformation—the called functions handle everything. This keeps the CLI dumb and the modules smart.

No command behavior

If the user runs pj with no subcommand, we print the help and exit with code 1. This is more helpful than an error message—the user sees all available commands immediately.

Future: Additional Commands

TODO: pj dev command

Launch development servers (Jupyter Lab, nbdev_preview) using the project’s venv jupyter. Should replace the post-init server launching currently inline in init_nbdev().

TODO: pj kernel command

Manage Jupyter kernels: list installed kernels, uninstall by name, re-register after moving projects. Common operations that currently require remembering jupyter kernelspec syntax.

TODO: pj check command

Run just the prerequisite checks without initializing a project. Useful for verifying your system is ready before starting a batch of projects.

These extensions are natural once the core workflow is solid. The modular structure makes adding new commands straightforward: create the function in an appropriate module, add the subparser here, dispatch to the function.

Examples

!cd .. && pwd && nbdev_export
/app/data/dev/pj
!/app/data/.local/bin/pj -h
usage: pj [-h] [--version] {init,sync,ship,kill} ...

pj > the ProJect shell (v0.0.5)
   ┌───────────────────────────
   │ Automate notebook project workflows: init, sync, and ship software.
   │ 🧬 https://kitled.github.io/pj        📜 Apache 2.0
   │ 📦 https://pypi.org/project/pj-sh     👨‍💻 Kit, 2025.
   └─

positional arguments:
  {init,sync,ship,kill}
                        Available commands
    init                Initialize a new nbdev project
    sync                Sync project: pull, prepare, and push to GitHub
    ship                Ship a new release: bump version, build, upload, tag,
                        and release
    kill                Kill all running background processes (jupyter,
                        nbdev_preview, quarto)

options:
  -h, --help            show this help message and exit
  --version, -V         show program's version number and exit

Examples:
  pj init my-project
  pj init my-project -v --desc "Awesome domain library" --private
  pj init my-project --python 3.11 --author "Upbeat Photon"
  pj sync
  pj sync -m "Added new feature"
  pj ship
  pj ship --dry-run
  pj kill
        
!/app/data/.local/bin/pj ship -h
usage: pj ship [-h] [--part {0,1,2}] [--dry-run] [--force] [--skip-pypi]
               [--skip-gh-release] [-v]

Ship a complete release in one command:
1. Check for uncommitted changes
2. Bump version with nbdev_bump_version
3. Commit and push version bump
4. Build and upload to PyPI with nbdev_pypi
5. Create git tag and push
6. Create GitHub release with auto-generated notes
        

options:
  -h, --help         show this help message and exit
  --part {0,1,2}     Version part to bump: 0=major, 1=minor, 2=patch (default:
                     2)
  --dry-run          Show what would be done without making changes
  --force            Ship even with uncommitted changes (not recommended)
  --skip-pypi        Skip PyPI upload (for testing)
  --skip-gh-release  Skip GitHub release creation
  -v, --verbose      Show detailed command output

Examples:
  pj ship                    # Bump patch version (0.0.X)
  pj ship --part 1           # Bump minor version (0.X.0)
  pj ship --part 0           # Bump major version (X.0.0)
  pj ship --dry-run          # Preview without making changes
  pj ship --skip-pypi        # Skip PyPI upload (for testing)
        
!/app/data/.local/bin/pj init -h
usage: pj init [-h] [-v] [--logfile LOGFILE] [--no-log] [--org ORG] [--public]
               [--description DESCRIPTION] [--python PYTHON] [--author AUTHOR]
               [--author-email AUTHOR_EMAIL]
               [--license {apache2,mit,gpl3,bsd3}] [--min-python MIN_PYTHON]
               [--no-preview] [--no-lab] [-c] [-q]
               name

Initialize a new nbdev project with all the bells and whistles:
- GitHub repository creation
- nbdev project structure with Jupyter hooks
- Virtual environment with uv
- Jupyter kernel registration
- direnv auto-activation
        

positional arguments:
  name                  Project name (will be repo and package name)

options:
  -h, --help            show this help message and exit

output options:
  -v, --verbose         Show detailed command output
  --logfile LOGFILE     Path to log file (default: PROJECT/init.log)
  --no-log              Disable logging to file

GitHub options:
  --org ORG             Create repository under this organization (default:
                        personal account)
  --public              Create public repository (default: private)
  --description DESCRIPTION, --desc DESCRIPTION
                        Repository description

Python options:
  --python PYTHON       Python version (e.g., 3.11, 3.12)

nbdev options:
  --author AUTHOR       Author name (default: from git config)
  --author-email AUTHOR_EMAIL
                        Author email (default: from git config)
  --license {apache2,mit,gpl3,bsd3}
                        License type (default: apache2)
  --min-python MIN_PYTHON
                        Minimum Python version (default: 3.9)

post-init options:
  --no-preview          Skip opening nbdev_preview
  --no-lab              Skip launching Jupyter Lab
  -c, --code            Open VSCode
  -q, --quiet           Quiet mode: skip preview and lab (just cd + tree)

Examples:
  pj init my-project
  pj init my-project -v --desc "ML utilities" --private
  pj init my-project --python 3.11 --license mit
        
!/app/data/.local/bin/pj sync -h
usage: pj sync [-h] [--message MESSAGE] [-v]

Sync your nbdev project in one command:
1. git pull (aborts on merge conflicts)
2. nbdev_prepare (export, test, clean - aborts if tests fail)
3. git commit -am "message"
4. git push
        

options:
  -h, --help            show this help message and exit
  --message MESSAGE, -m MESSAGE
                        Commit message (default: 'save')
  -v, --verbose         Show detailed command output

Examples:
  pj sync                    # Uses default message "save"
  pj sync -m "Added tests"   # Custom commit message
  pj sync -v                 # Verbose output