!cd .. && pwd && nbdev_export/app/data/dev/pj
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.
The main entry point constructs the argument parser hierarchy: a root parser with subcommands, each subcommand with its own arguments and flags.
main ()
Main CLI entry point
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.
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.
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
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)
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
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