Metadata-Version: 2.4
Name: fastws-cli
Version: 0.0.9
Summary: Fast workspace tools for multi-repo management
Author: Jeremy Howard
License: Apache-2.0
Project-URL: Homepage, https://github.com/AnswerDotAI/fastws
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastcore>=1.14.3
Requires-Dist: fastgit>=0.0.7
Requires-Dist: ghapi>=2.0.6
Requires-Dist: packaging
Requires-Dist: tomli; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: fastship; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# fastws

Fast workspace tools for multi-repo management.

## Install

```bash
pip install fastws-cli
```

## Setup

Create a `repos.txt` file listing your repos (one per line):

```
AnswerDotAI/fastcore
AnswerDotAI/fastgit
AnswerDotAI/fastship
AnswerDotAI/fastws
```

A line may add a checkout location after the repo, for repos that live outside
the workspace root (`~` ok). Every command then reads and writes them there, and
`ws-sync` editable-installs each Python project found inside (the repo root if it
has a `pyproject.toml`, else its immediate subdirectories), so one out-of-tree
repo can hold several small packages alongside non-Python content:

```
AnswerDotAI/fastcore
jph00/private ~/private
```

## Commands

### `ws-clone`

Clone all repos from your repos file:

```bash
ws-clone
ws-clone --repos-file myrepos.txt
ws-clone --workers 8
```

### `ws-pull`

Pull updates for all repos (parallel):

```bash
ws-pull
```

### `ws-status`

Show uncommitted changes and unpushed commits:

```bash
ws-status
ws-status --branches
```

### `ws-branches`

Check if all repos are on the expected branch:

```bash
ws-branches
ws-branches --expected develop
```

### `ws-build`

Build an sdist of each workspace project (including `repos.txt` checkouts outside the root) into a dists directory, `.dists` by default. A project is rebuilt only when a file in it is newer than its existing sdist, and older versions of a rebuilt package are pruned, so the directory always holds one current sdist per project. A failing project gets a warning without stopping the others, but the command then exits 1.

Progress goes to stderr; on success the dists path prints to stdout, so `$(ws-build)` in a script both refreshes the pool and yields its location. The result suits any resolver that takes a candidate pool, e.g. `uv pip install --find-links` or a docker build context for images that must test unreleased workspace packages:

```bash
ws-build
ws-build --force  # rebuild everything
ws-build --out /tmp/dists
```

### `ws-sync`

Sync the workspace metadata, pull local repos, and install updates.
By default it uses the active venv parent as the workspace root, so you do not need to `cd` first.
It respects `tool.uv.workspace.members` and `exclude` when scanning local projects, and if any
member directory isn't a Python project yet (no `pyproject.toml`, e.g. a fresh empty clone), it
warns and skips the `uv sync` step instead of letting uv fail on the whole workspace.

The workspace `exclude` list is auto-managed: a top-level directory that isn't a valid Python
project and isn't a `repos.txt` checkout gets excluded automatically, and is un-excluded once it
gains a real `pyproject.toml`. Globs, entries for missing directories, and entries for `repos.txt`
checkouts are always kept, and `exclude = [...]` under `[tool.fastws]` in the workspace
`pyproject.toml` declares intent the scan can't infer (e.g. keeping a real project out of the
workspace). Hand-written `[tool.uv.sources]` entries (path, git, ...) are preserved when syncing
adds new members.

At most once per day (tracked by a stamp file inside the workspace's `.git`, so git never
sees it), the sync also floats dependencies: `uv sync -U` instead of plain `uv sync`, plus
a parallel `cargo update` in every member with a `Cargo.toml`, printing what moved. Pass
`--upgrade` to force that pass regardless of when it last ran.

Before every uv sync, `ws-sync` writes `.git/fastws-cargo-key` for each member crate. The key hashes `Cargo.lock` contents and the workspace Cargo patch configuration. For Git dependencies redirected to local paths by `[patch."<url>"]`, it also hashes each patched crate's `Cargo.toml`, `build.rs`, and `src` tree, recursively. The file is rewritten only when that content changes, so projects can use `{ file = ".git/fastws-cargo-key" }` in `tool.uv.cache-keys` without rebuilding after a timestamp-only `Cargo.lock` write.

```bash
ws-sync
ws-sync --workspace ~/aai-ws
ws-sync --upgrade
```

### `ws-add`

Add a repo to `repos.txt`, then run `ws-sync`. Given `owner/repo`, it clones; given the
name of an existing local folder (e.g. one just scaffolded with `nbdev-new` or `ship-new`),
it resolves `owner/repo` from the folder's `origin` remote instead, telling you exactly
what's missing if the folder has no git repo, no GitHub origin, or no `pyproject.toml`.
Given a path outside the workspace root, the repo stays where it is and its location is
recorded in `repos.txt` (no root `pyproject.toml` needed: its packages are discovered on sync):

```bash
ws-add AnswerDotAI/fastws
ws-add answerdotai/fastws
ws-add fastws  # existing local folder, resolved via its origin remote
ws-add ~/private  # a path outside the workspace: stays where it is, recorded in repos.txt with its location
```

### `ws-remove`

Remove a repo: delete its clone, and drop it from `repos.txt` and the workspace
`pyproject.toml`, then run `uv sync`. It refuses if the directory has uncommitted
changes, unpushed commits, no `origin` remote, or isn't a clean git checkout, and
always prompts for confirmation before deleting anything:

```bash
ws-remove AnswerDotAI/fastws
ws-remove fastws  # bare folder name also works if the directory exists
```

### `ws-releases`

Report repos with commits since their newest GitHub release, so nothing reviewed sits unshipped:

```bash
ws-releases                 # at the workspace root: sweep every repo in repos.txt
ws-releases solveit         # only solveit's transitive workspace dependencies
ws-releases --nodeps        # inside a repo: just that repo
ws-releases --skip 'wip'    # extra start-of-message regex for commits that need no release
```

Run from inside a workspace checkout, the sweep narrows to that repo and its transitive workspace dependencies; `--nodeps` narrows it to the repo alone (an error when no project is in play).

Each pending repo lists its unreleased commit summaries; repos with no releases yet get one quiet `no releases:` line, and fully-released repos appear in `up to date:`. The newest release is picked by version number (publish timestamps can be out of order), and repos whose default branch isn't `main` are handled automatically.

Commits whose message matches a start-anchored regex from the skip set need no release and aren't reported. The built-in set covers version bumps and housekeeping (`bump`, `nbdev regen`, `.gitignore`, `docs`, `CI`, ...: `DEFAULT_SKIP` in `fastws.releases`); `[tool.fastws]` in the workspace root `pyproject.toml` adds to it, and names repos that should never be swept (apps that deploy rather than release):

```toml
[tool.fastws]
release_skip = ["docs only"]
release_exclude = ["solveit", "md_site"]
```

From Python (the primary interface - the CLI is a thin wrapper over `fastws.releases`):

```python
from fastws import check_releases, check_release
await check_releases()            # ReleaseReport: the repr is the report
await check_releases('solveit')   # dependency-closure mode
await check_release('mdhtml')     # one repo: list of unreleased commit summaries (None = no releases)
```
