Skip to main content

A terminal tool for managing experimental Python projects

Project description

novo

novo

Spin up Python experiments in seconds, from anywhere in your terminal.

Install once with uv, then use novo from any directory to scaffold isolated, git-tracked Python experiments from reusable seed templates — drive it from the command line or a built-in TUI.

PyPI Python 3.11+ Built with uv License: MIT Changelog


Install

novo is distributed as a uv tool — installed once into an isolated environment and exposed on your PATH.

Prerequisite: uv and git.

# from PyPI
uv tool install novo

# or from this repo
uv tool install git+https://github.com/fmeiraf/novo

If novo isn't found after install, run uv tool update-shell once to add ~/.local/bin to your PATH.

novo --version

That's it — the novo source tree is no longer needed. Use novo from any directory.


First steps

1. Pick a workspace (optional)

novo stores experiments in a workspace — a directory marked with .novo/ (same pattern as git's .git/). You don't have to set one up to get started: novo new works from any directory.

When you run any novo command, it resolves the active workspace in this order:

  1. cwd discovery — walks up from the current directory looking for a .novo/ marker. First hit wins.
  2. configured home — if nothing is found, falls back to workspace.path in ~/.config/novo/config.toml (only if you've set it).
  3. auto-detached — if neither resolves, novo runs in detached mode against the current directory. novo new drops a self-contained experiment in cwd; read-only commands refuse with a novo init tip.

So novo new churn-analysis in a brand-new terminal just works — the experiment lands right next to you, as its own self-contained project. novo prints a one-line hint the first time so you know what happened:

ℹ no workspace found here — running in detached mode (run `novo init` to set up a workspace).

Want experiments to live in a specific tree (e.g. one per project)? Turn any directory into a workspace:

cd ~/code/experiments
novo init

novo init writes the .novo/ marker and initializes the directory as a git repo. From then on, every novo command run inside that tree (or any subdirectory) operates on that workspace.

You can also point at a workspace explicitly with --workspace <path> or NOVO_WORKSPACE=<path>, or force detached mode with --detached (see Detached mode).

2. Create your first experiment

novo new image-classifier --tag ml --desc "ResNet experiments"
novo new quick-spike --no-date                # skip the YYYY-MM-DD- prefix
novo new pinned-run --python 3.12 --seed user:data-science

novo creates a date-prefixed directory inside your workspace, sets up a uv project, applies the default seed, and commits the result. Pass --no-date (per experiment) or novo config set naming.date_prefix false (globally) to drop the date prefix; pass --python <ver> to override defaults.python.

3. Set up a seed (optional)

Seeds are reusable project templates — your favorite stack, scripts, and config files copied into every new experiment.

novo seed list                                          # see what's available
novo seed init data-science                             # scaffold a new empty seed
novo seed init data-science --desc "Numpy + pandas"     # with a description
novo seed init data-science --scope user                # force user scope
novo seed init data-science --path ./my-registry/etl    # write the seed to a custom dir

By default seed init writes to the workspace's .novo/seeds/ when cwd is inside a workspace, otherwise to ~/.local/share/novo/seeds/. Use --scope local/--scope user to force one, or --path <dir> to drop the seed anywhere on disk — that last form is what you want when authoring a remote registry.

Without --path, seed init data-science creates ~/.local/share/novo/seeds/data-science/ with a seed.toml manifest and a template/ directory. Edit the manifest to declare dependencies and post-create hooks:

[seed]
name = "data-science"
description = "Numpy + pandas + jupyter starter"

[seed.dependencies]
packages = ["numpy", "pandas", "jupyter"]

[seed.post_create]
commands = ["mkdir notebooks"]

Drop starter files into template/ — they'll be copied into every new experiment that uses the seed:

novo new churn-analysis --seed data-science

4. Use seeds from a team registry (optional)

A remote seed registry is a git repo whose top-level subdirectories each contain a seed. One repo can hold many seeds:

team-seeds/
├── etl/
│   ├── seed.toml
│   └── template/
├── ml-experiment/
│   ├── seed.toml
│   └── template/
└── ...

Link it once with a local name:

novo seed link git@github.com:team/novo-seeds.git --name team

Every seed in the registry is now available as remote:team/<seed-name>:

novo new pricing-model --seed remote:team/ml-experiment
novo seed sync team        # pull updates later
novo seed unlink team      # drop the link

Authoring your own registry

A registry is just a git repo whose top-level subdirectories each contain a seed (one seed.toml per subdir). Use seed init --path to scaffold seeds directly into the repo — no need to bounce through user or local scope first:

git clone git@github.com:team/novo-seeds.git
cd novo-seeds

novo seed init etl            --path ./etl            --desc "Standard ETL stack"
novo seed init ml-experiment  --path ./ml-experiment  --desc "Pytorch + MLflow starter"

# edit the generated seed.toml + template/ in each directory, then commit + push
git add . && git commit -m "add etl + ml-experiment seeds" && git push

Anyone who's already linked the registry can pull your additions with novo seed sync <name> (or r in the TUI's Seeds tab).

See docs/seeds.md for scope resolution, identifier syntax, and the full registry layout.


Configuration

novo works out of the box, but a handful of settings let you tailor the defaults. They live in a single TOML file at ~/.config/novo/config.toml (XDG-compliant; the exact path depends on your OS).

Key Type Default What it does
workspace.path str "" (none) Optional "home" workspace used when cwd discovery finds nothing. Empty = no home workspace; commands auto-detach in cwd. Set explicitly with novo config set workspace.path <path>.
defaults.seed str "default" Seed used when novo new is called without --seed. Accepts scoped forms (e.g. "user:data-science", "remote:team/etl").
defaults.python str "" (system) Python version passed to uv init for new experiments (e.g. "3.12"). Override per-experiment with --python.
defaults.auto_commit bool true Auto-commit the workspace on novo new / novo delete.
defaults.detached_git bool true In detached mode, init a git repo inside each created experiment and commit.
naming.date_prefix bool true Prefix experiment directories with today's date (2026-05-06-foo). Skip per-experiment with --no-date.
[[seeds.remotes]] table-list [] Linked remote seed registries. Managed via novo seed link / unlink. See docs/seeds.md for registry layout.

Inspecting and changing settings

novo config show                          # table of all keys
novo config get defaults.python           # read one key
novo config set defaults.python 3.12      # write one key
novo config set naming.date_prefix false  # accepts true/false, yes/no, 1/0, on/off

You can also edit ~/.config/novo/config.toml directly:

[workspace]
path = "/Users/me/code/experiments"

[defaults]
seed = "user:data-science"
python = "3.12"
auto_commit = true
detached_git = true

[naming]
date_prefix = true

[[seeds.remotes]]
name = "team"
url = "git@github.com:team/novo-seeds.git"
ref = ""   # empty = follow the cloned branch

Pinning a Python version

A seed itself can't declare a Python version today — it's resolved per-experiment in this order:

  1. --python flag on novo new
  2. defaults.python in config.toml
  3. Whatever uv picks as the system default

So to make every new experiment use 3.12 by default:

novo config set defaults.python 3.12
novo new quick-test          # uses 3.12
novo new legacy --python 3.10  # one-off override

The TUI

Run novo with no arguments to launch the interactive terminal UI — a Textual app for browsing, searching, and managing experiments without memorizing flags.

 ┌─ Experiments ─────────────┐ ┌─ Details ──────────────────┐
 │  > 2026-04-21-transformer  │ │  Name: transformer-exp     │
 │    2026-04-20-image-cls    │ │  Seed: ml-stack            │
 │    2026-04-19-data-clean   │ │  Tags: nlp, pytorch        │
 └────────────────────────────┘ └────────────────────────────┘
  n new  d delete  s seeds  / search  Enter open  ? help  q quit
Key Action
n Create new experiment
d Delete selected
s Switch to Seeds tab
e Switch to Experiments tab
/ Search
Enter Open experiment in a new terminal window
j / k Navigate (vim-style)
? Help
q Quit

Extra bindings on the Seeds tab: N scaffold seed, l link remote registry, u unlink, r sync remotes, t focus the template tree.

See docs/tui.md for the full screen and widget breakdown.


Detached mode

Sometimes you don't want a workspace at all — just a one-off, self-contained experiment wherever you happen to be standing. That's what --detached is for.

cd ~/scratch
novo --detached new spike-idea

What you get:

  • A fresh project directory at ~/scratch/spike-idea/ with its own uv env and seed-applied files.
  • Its own git repo + initial commit (when defaults.detached_git = true, the default).
  • No registration anywhere — novo's workspace listing won't track it. novo list / info / search / delete / open deliberately refuse under --detached to keep the boundary clear.

Use --at <path> to drop the experiment somewhere other than cwd:

novo --detached new spike-idea --at ~/tmp

Running novo --detached with no subcommand launches a minimal TUI landing screen with the actions that make sense outside a workspace: new experiment here, link a remote registry, init a workspace here, browse seeds (CLI hint).

Drop --detached (or run novo init once in that directory) when you want to go back to workspace mode.


Everyday commands

novo                                       # launch the interactive TUI
novo new <name>                            # create an experiment
novo list                                  # list experiments
novo info [name]                           # workspace summary, or details for one experiment
novo search <query>                        # fuzzy search across name, description, tags
novo open <name>                           # cd into an experiment (needs shell integration)
novo delete <name>
novo init [path]                           # write a `.novo/` marker at path (default cwd)
novo seed list | init <name>               # browse / scaffold seeds
novo seed link <url>                       # link a remote seed registry (idempotent)
novo seed sync [<name>] | unlink <name>    # refresh or remove a linked remote
novo config show | get <key> | set <key> <value>
novo --workspace <path> <cmd>              # operate on a specific workspace
novo --detached new <name>                 # create a self-contained experiment in cwd

For novo open to actually cd, add this to your ~/.zshrc / ~/.bashrc:

eval "$(novo --shell-init)"

Updating & uninstalling

uv tool upgrade novo
uv tool uninstall novo

Documentation

Deeper docs live in docs/:

Document Description
architecture.md Layers, data flow, project structure
cli.md All CLI commands and flags
tui.md Textual app, screens, keybindings
core.md Experiment, seed, config, workspace, remote, git logic
seeds.md Seed scopes, identifier syntax, remote sync workflow
models.md Pydantic schemas
development.md Local dev setup
testing.md Test layout and conventions

Development

Work on novo itself with an editable install so changes are picked up immediately:

git clone https://github.com/fmeiraf/novo
cd novo
uv tool install --editable .
uv run pytest

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

novo-0.2.0.tar.gz (40.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

novo-0.2.0-py3-none-any.whl (60.2 kB view details)

Uploaded Python 3

File details

Details for the file novo-0.2.0.tar.gz.

File metadata

  • Download URL: novo-0.2.0.tar.gz
  • Upload date:
  • Size: 40.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for novo-0.2.0.tar.gz
Algorithm Hash digest
SHA256 37d3b1a381c27e3cf210aed04c4cd6c2fb5f747c03400a9a6a3b6f3ddf672a73
MD5 970721d871cbee592bd51896cf5b9bb6
BLAKE2b-256 9e7b3b144f6b86757085ba2da8fdbf80849083541236a7420991a86b3dc86506

See more details on using hashes here.

Provenance

The following attestation bundles were made for novo-0.2.0.tar.gz:

Publisher: pypi_workflow.yml on fmeiraf/novo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file novo-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: novo-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 60.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for novo-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cafa58348d01859262db72c490fbd0f12d570dfe5b5ccb6b1f60f093b6f81032
MD5 4fcc97c686baee3886f8085f4a082f6d
BLAKE2b-256 45203f0f873da8fafffd2078df46a280b7cfd52f5466e2b69a909cffd13822cf

See more details on using hashes here.

Provenance

The following attestation bundles were made for novo-0.2.0-py3-none-any.whl:

Publisher: pypi_workflow.yml on fmeiraf/novo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page