update
This commit is contained in:
52
.github/workflows/release.yml
vendored
Normal file
52
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python 3.13
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Build release package
|
||||||
|
run: make release-package
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: flow-python-release
|
||||||
|
path: |
|
||||||
|
dist/flow-*-python.tar.gz
|
||||||
|
dist/flow-*-python.tar.gz.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Publish release assets
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||||
|
gh release upload "$TAG" dist/flow-*-python.tar.gz dist/flow-*-python.tar.gz.sha256 --clobber
|
||||||
|
else
|
||||||
|
gh release create "$TAG" dist/flow-*-python.tar.gz dist/flow-*-python.tar.gz.sha256 --title "$TAG" --generate-notes
|
||||||
|
fi
|
||||||
16
Makefile
16
Makefile
@@ -6,8 +6,11 @@ BUILD_DIR := build
|
|||||||
SPEC_FILE := flow.spec
|
SPEC_FILE := flow.spec
|
||||||
BINARY := $(DIST_DIR)/flow
|
BINARY := $(DIST_DIR)/flow
|
||||||
INSTALL_DIR ?= $(HOME)/.local/bin
|
INSTALL_DIR ?= $(HOME)/.local/bin
|
||||||
|
VERSION := $(shell sed -n 's/^__version__ = "\(.*\)"/\1/p' src/flow/__init__.py)
|
||||||
|
RELEASE_ROOT := $(BUILD_DIR)/flow-$(VERSION)-python
|
||||||
|
RELEASE_ARCHIVE := $(DIST_DIR)/flow-$(VERSION)-python.tar.gz
|
||||||
|
|
||||||
.PHONY: help deps test test-e2e check package build binary install install-local check-binary clean distclean
|
.PHONY: help deps test test-e2e check package release-package build binary install install-local check-binary clean distclean
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@printf "Targets:\n"
|
@printf "Targets:\n"
|
||||||
@@ -16,6 +19,7 @@ help:
|
|||||||
@printf " make test-e2e Run Docker/Podman e2e tests (FLOW_RUN_E2E=1)\n"
|
@printf " make test-e2e Run Docker/Podman e2e tests (FLOW_RUN_E2E=1)\n"
|
||||||
@printf " make check Run unit tests and CLI smoke check\n"
|
@printf " make check Run unit tests and CLI smoke check\n"
|
||||||
@printf " make package Build wheel and sdist into dist/\n"
|
@printf " make package Build wheel and sdist into dist/\n"
|
||||||
|
@printf " make release-package Build installable Python release tarball\n"
|
||||||
@printf " make build Build standalone binary at dist/flow\n"
|
@printf " make build Build standalone binary at dist/flow\n"
|
||||||
@printf " make install Build and install binary to ~/.local/bin/flow\n"
|
@printf " make install Build and install binary to ~/.local/bin/flow\n"
|
||||||
@printf " make check-binary Run dist/flow --help\n"
|
@printf " make check-binary Run dist/flow --help\n"
|
||||||
@@ -38,6 +42,16 @@ check: test
|
|||||||
package: deps
|
package: deps
|
||||||
$(UV) build
|
$(UV) build
|
||||||
|
|
||||||
|
release-package: deps
|
||||||
|
rm -rf "$(RELEASE_ROOT)" "$(RELEASE_ARCHIVE)" "$(RELEASE_ARCHIVE).sha256"
|
||||||
|
mkdir -p "$(RELEASE_ROOT)/wheelhouse"
|
||||||
|
$(UV) build
|
||||||
|
cp dist/flow-$(VERSION)-py3-none-any.whl "$(RELEASE_ROOT)/wheelhouse/"
|
||||||
|
cp packaging/install.sh "$(RELEASE_ROOT)/install.sh"
|
||||||
|
chmod 755 "$(RELEASE_ROOT)/install.sh"
|
||||||
|
tar -czf "$(RELEASE_ARCHIVE)" -C "$(BUILD_DIR)" "flow-$(VERSION)-python"
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$(RELEASE_ARCHIVE)" > "$(RELEASE_ARCHIVE).sha256"; else shasum -a 256 "$(RELEASE_ARCHIVE)" > "$(RELEASE_ARCHIVE).sha256"; fi
|
||||||
|
|
||||||
build binary: deps
|
build binary: deps
|
||||||
$(UV) run pyinstaller --noconfirm --clean --onefile --name flow --paths "$(SRC_DIR)" "$(ENTRYPOINT)"
|
$(UV) run pyinstaller --noconfirm --clean --onefile --name flow --paths "$(SRC_DIR)" "$(ENTRYPOINT)"
|
||||||
|
|
||||||
|
|||||||
79
README.md
79
README.md
@@ -6,7 +6,32 @@ completion.
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
From the repository:
|
Install from a release tarball (no `uv`, no `pipx`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VERSION=0.1.0
|
||||||
|
REPO=OWNER/REPO
|
||||||
|
|
||||||
|
curl -fsSL "https://github.com/${REPO}/releases/download/v${VERSION}/flow-${VERSION}-python.tar.gz" \
|
||||||
|
-o "/tmp/flow-${VERSION}-python.tar.gz"
|
||||||
|
curl -fsSL "https://github.com/${REPO}/releases/download/v${VERSION}/flow-${VERSION}-python.tar.gz.sha256" \
|
||||||
|
-o "/tmp/flow-${VERSION}-python.tar.gz.sha256"
|
||||||
|
sha256sum -c "/tmp/flow-${VERSION}-python.tar.gz.sha256"
|
||||||
|
tar -xzf "/tmp/flow-${VERSION}-python.tar.gz" -C /tmp
|
||||||
|
/tmp/flow-${VERSION}-python/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternative one-shot path with `curl` and shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmpdir"' EXIT
|
||||||
|
curl -fsSL "https://github.com/${REPO}/releases/download/v${VERSION}/flow-${VERSION}-python.tar.gz" \
|
||||||
|
| tar -xz -C "$tmpdir"
|
||||||
|
sh "${tmpdir}/flow-${VERSION}-python/install.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
From the repository (development):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync --locked --extra dev --extra build
|
uv sync --locked --extra dev --extra build
|
||||||
@@ -23,13 +48,6 @@ flow setup show linux-work
|
|||||||
flow setup run linux-work --dry-run
|
flow setup run linux-work --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
Install the CLI as a local uv tool:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv tool install --force .
|
|
||||||
flow --help
|
|
||||||
```
|
|
||||||
|
|
||||||
Build a standalone binary:
|
Build a standalone binary:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -39,8 +57,42 @@ make install # installs dist/flow to ~/.local/bin/flow
|
|||||||
```
|
```
|
||||||
|
|
||||||
`build/`, `dist/`, and `flow.spec` are generated by packaging/binary builds and
|
`build/`, `dist/`, and `flow.spec` are generated by packaging/binary builds and
|
||||||
are ignored. `.venv/` is the uv-managed local environment. `venv/` is legacy
|
are ignored. `.venv/` and `venv/` are local virtual environments and are also
|
||||||
local clutter and should not be used.
|
ignored.
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
The primary release artifact is a Python tarball published from GitHub Releases:
|
||||||
|
|
||||||
|
```text
|
||||||
|
flow-<version>-python.tar.gz
|
||||||
|
flow-<version>-python.tar.gz.sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
The tarball includes `install.sh` plus the `flow` wheel. End users need
|
||||||
|
`python3` with `venv`; they do not need `uv` or `pipx`. The installer uses
|
||||||
|
`pip` inside the isolated venv to install `flow` and resolve Python
|
||||||
|
dependencies.
|
||||||
|
|
||||||
|
Default install locations:
|
||||||
|
|
||||||
|
- app environment: `~/.local/share/flow/venv`
|
||||||
|
- command shim: `~/.local/bin/flow`
|
||||||
|
|
||||||
|
Overrides:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHON=/path/to/python3 FLOW_INSTALL_ROOT=~/.flow FLOW_BIN_DIR=~/bin ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Release build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make release-package
|
||||||
|
```
|
||||||
|
|
||||||
|
Native one-file binaries remain optional convenience artifacts. They are
|
||||||
|
platform-specific; the Python release tarball is the portable default.
|
||||||
|
|
||||||
## Command Surface
|
## Command Surface
|
||||||
|
|
||||||
@@ -71,7 +123,6 @@ flow setup run [PROFILE|--profile NAME] [--dry-run] [--var KEY=VALUE]
|
|||||||
# Remote targets
|
# Remote targets
|
||||||
flow remote list
|
flow remote list
|
||||||
flow remote enter TARGET [--user USER] [--namespace NAME] [--platform NAME] [--session NAME] [--no-tmux] [--dry-run]
|
flow remote enter TARGET [--user USER] [--namespace NAME] [--platform NAME] [--session NAME] [--no-tmux] [--dry-run]
|
||||||
flow enter TARGET
|
|
||||||
|
|
||||||
# Dev containers
|
# Dev containers
|
||||||
flow dev create NAME --image IMAGE [--project PATH] [--dry-run]
|
flow dev create NAME --image IMAGE [--project PATH] [--dry-run]
|
||||||
@@ -87,7 +138,7 @@ flow dev list
|
|||||||
flow projects check [--fetch]
|
flow projects check [--fetch]
|
||||||
flow projects fetch
|
flow projects fetch
|
||||||
flow projects summary
|
flow projects summary
|
||||||
flow sync
|
flow projects sync
|
||||||
|
|
||||||
# Shell completion
|
# Shell completion
|
||||||
flow completion zsh
|
flow completion zsh
|
||||||
@@ -104,9 +155,8 @@ Aliases:
|
|||||||
- `dotfiles` -> `dot`
|
- `dotfiles` -> `dot`
|
||||||
- `dotfiles repos` -> `dotfiles repo`
|
- `dotfiles repos` -> `dotfiles repo`
|
||||||
- `packages` -> `package`, `pkg`
|
- `packages` -> `package`, `pkg`
|
||||||
- `projects` -> `project`; `flow sync` -> `flow projects check --fetch`
|
- `projects` -> `project`
|
||||||
- `setup` -> `bootstrap`, `provision`
|
- `setup` -> `bootstrap`, `provision`
|
||||||
- `remote enter` -> `enter`
|
|
||||||
- `dev attach` -> `dev connect`
|
- `dev attach` -> `dev connect`
|
||||||
- `dev remove` -> `dev rm`
|
- `dev remove` -> `dev rm`
|
||||||
|
|
||||||
@@ -240,6 +290,7 @@ make test # unit tests, excluding e2e
|
|||||||
make test-e2e # requires Docker or healthy Podman
|
make test-e2e # requires Docker or healthy Podman
|
||||||
make check # tests plus CLI smoke checks
|
make check # tests plus CLI smoke checks
|
||||||
make package # wheel and sdist in dist/
|
make package # wheel and sdist in dist/
|
||||||
|
make release-package # installable GitHub Release tarball
|
||||||
make build # PyInstaller binary in dist/flow
|
make build # PyInstaller binary in dist/flow
|
||||||
make clean # remove build/test artifacts
|
make clean # remove build/test artifacts
|
||||||
make distclean # also remove .venv/ and venv/
|
make distclean # also remove .venv/ and venv/
|
||||||
|
|||||||
@@ -68,10 +68,8 @@ flow dotfiles repos list|status|pull|push
|
|||||||
flow packages install|remove|list
|
flow packages install|remove|list
|
||||||
flow setup run|show|list
|
flow setup run|show|list
|
||||||
flow remote enter|list
|
flow remote enter|list
|
||||||
flow enter
|
|
||||||
flow dev create|attach|connect|exec|enter|stop|remove|rm|respawn|list
|
flow dev create|attach|connect|exec|enter|stop|remove|rm|respawn|list
|
||||||
flow projects check|fetch|summary
|
flow projects check|fetch|summary|sync
|
||||||
flow sync
|
|
||||||
flow completion zsh|install-zsh
|
flow completion zsh|install-zsh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
71
packaging/install.sh
Normal file
71
packaging/install.sh
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
PYTHON_BIN="${PYTHON:-python3}"
|
||||||
|
INSTALL_ROOT="${FLOW_INSTALL_ROOT:-$HOME/.local/share/flow}"
|
||||||
|
BIN_DIR="${FLOW_BIN_DIR:-$HOME/.local/bin}"
|
||||||
|
VENV_DIR="$INSTALL_ROOT/venv"
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
WHEELHOUSE="$SCRIPT_DIR/wheelhouse"
|
||||||
|
|
||||||
|
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "python3 is required. Set PYTHON=/path/to/python3 to override." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! "$PYTHON_BIN" -m venv --help >/dev/null 2>&1; then
|
||||||
|
echo "python3 was built without the venv module. Install Python's venv package." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! "$PYTHON_BIN" -m pip --version >/dev/null 2>&1; then
|
||||||
|
echo "python3 pip module is required. Install pip for the selected python." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$WHEELHOUSE" ]; then
|
||||||
|
echo "wheelhouse not found: $WHEELHOUSE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FLOW_WHEELS_COUNT=0
|
||||||
|
FLOW_WHEEL=""
|
||||||
|
for candidate in "$WHEELHOUSE"/flow-*.whl; do
|
||||||
|
if [ -f "$candidate" ]; then
|
||||||
|
FLOW_WHEELS_COUNT=$((FLOW_WHEELS_COUNT + 1))
|
||||||
|
if [ -z "$FLOW_WHEEL" ]; then
|
||||||
|
FLOW_WHEEL="$candidate"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$FLOW_WHEELS_COUNT" -eq 0 ]; then
|
||||||
|
echo "flow wheel not found in $WHEELHOUSE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$FLOW_WHEELS_COUNT" -gt 1 ]; then
|
||||||
|
echo "multiple flow wheels found in $WHEELHOUSE. Ensure exactly one release wheel is present." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$INSTALL_ROOT"
|
||||||
|
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
||||||
|
|
||||||
|
if ! "$VENV_DIR/bin/python" -m pip --version >/dev/null 2>&1; then
|
||||||
|
echo "Created venv does not expose pip. Ensure Python's ensurepip is available." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$VENV_DIR/bin/python" -m pip install --no-cache-dir --upgrade "$FLOW_WHEEL"
|
||||||
|
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
cat > "$BIN_DIR/flow" <<EOF
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
exec "$VENV_DIR/bin/flow" "\$@"
|
||||||
|
EOF
|
||||||
|
chmod 755 "$BIN_DIR/flow"
|
||||||
|
|
||||||
|
echo "Installed flow to $BIN_DIR/flow"
|
||||||
|
echo "Run: $BIN_DIR/flow --version"
|
||||||
@@ -71,7 +71,6 @@ def _canonical_command(command: str) -> str:
|
|||||||
"package": "packages",
|
"package": "packages",
|
||||||
"pkg": "packages",
|
"pkg": "packages",
|
||||||
"project": "projects",
|
"project": "projects",
|
||||||
"sync": "projects",
|
|
||||||
}
|
}
|
||||||
return aliases.get(command, command)
|
return aliases.get(command, command)
|
||||||
|
|
||||||
@@ -330,7 +329,7 @@ def _complete_packages(ctx: FlowContext, before: Sequence[str], current: str) ->
|
|||||||
|
|
||||||
def _complete_projects(before: Sequence[str], current: str) -> list[str]:
|
def _complete_projects(before: Sequence[str], current: str) -> list[str]:
|
||||||
if len(before) <= 1:
|
if len(before) <= 1:
|
||||||
return _filter(["check", "fetch", "summary"], current)
|
return _filter(["check", "fetch", "summary", "sync"], current)
|
||||||
if before[1] == "check" and current.startswith("-"):
|
if before[1] == "check" and current.startswith("-"):
|
||||||
return _filter(["--fetch", "--no-fetch"], current)
|
return _filter(["--fetch", "--no-fetch"], current)
|
||||||
return []
|
return []
|
||||||
|
|||||||
136
src/flow/cli.py
136
src/flow/cli.py
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
from typer.main import TyperGroup
|
||||||
|
|
||||||
from flow import __version__
|
from flow import __version__
|
||||||
from flow.core import paths
|
from flow.core import paths
|
||||||
@@ -23,21 +26,114 @@ from flow.app.projects import ProjectService
|
|||||||
from flow.app.remote import RemoteService
|
from flow.app.remote import RemoteService
|
||||||
|
|
||||||
|
|
||||||
|
_HELP_CONTEXT = {"help_option_names": ["-h", "--help"]}
|
||||||
|
|
||||||
|
_ALIASED_COMMANDS: dict[str, str] = {
|
||||||
|
"dot": "dotfiles",
|
||||||
|
"package": "packages",
|
||||||
|
"pkg": "packages",
|
||||||
|
"bootstrap": "setup",
|
||||||
|
"provision": "setup",
|
||||||
|
"repo": "repos",
|
||||||
|
"project": "projects",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _AliasGroupingTyperGroup(TyperGroup):
|
||||||
|
"""Render aliased commands as a single help entry."""
|
||||||
|
|
||||||
|
def _grouped_command_names(self, ctx: typer.Context) -> OrderedDict[str, str]:
|
||||||
|
grouped: OrderedDict[str, list[str]] = OrderedDict()
|
||||||
|
for subcommand in super().list_commands(ctx):
|
||||||
|
cmd = super().get_command(ctx, subcommand)
|
||||||
|
if cmd is None or cmd.hidden:
|
||||||
|
continue
|
||||||
|
canonical_name = _ALIASED_COMMANDS.get(subcommand, subcommand)
|
||||||
|
grouped.setdefault(canonical_name, []).append(subcommand)
|
||||||
|
|
||||||
|
commands: OrderedDict[str, str] = OrderedDict()
|
||||||
|
for canonical_name, subcommands in grouped.items():
|
||||||
|
aliases = sorted(name for name in set(subcommands) if name != canonical_name)
|
||||||
|
display = ", ".join([canonical_name] + aliases)
|
||||||
|
commands[display] = canonical_name
|
||||||
|
|
||||||
|
# Keep insertion order by canonical key
|
||||||
|
return commands
|
||||||
|
|
||||||
|
def list_commands(self, ctx: typer.Context) -> list[str]:
|
||||||
|
return list(self._grouped_command_names(ctx).keys())
|
||||||
|
|
||||||
|
def get_command(self, ctx: typer.Context, cmd_name: str) -> object | None:
|
||||||
|
grouped = self._grouped_command_names(ctx)
|
||||||
|
canonical_name = grouped.get(cmd_name)
|
||||||
|
if canonical_name is None:
|
||||||
|
return super().get_command(ctx, cmd_name)
|
||||||
|
|
||||||
|
command = super().get_command(ctx, canonical_name)
|
||||||
|
if command is None:
|
||||||
|
return None
|
||||||
|
if "," in cmd_name:
|
||||||
|
command = copy.copy(command)
|
||||||
|
command.name = cmd_name
|
||||||
|
return command
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
|
cls=_AliasGroupingTyperGroup,
|
||||||
name="flow",
|
name="flow",
|
||||||
help="DevFlow - development environment manager",
|
help="DevFlow - development environment manager",
|
||||||
no_args_is_help=True,
|
no_args_is_help=True,
|
||||||
add_completion=False,
|
add_completion=False,
|
||||||
pretty_exceptions_show_locals=False,
|
pretty_exceptions_show_locals=False,
|
||||||
|
context_settings=_HELP_CONTEXT,
|
||||||
|
)
|
||||||
|
dotfiles_app = typer.Typer(
|
||||||
|
cls=_AliasGroupingTyperGroup,
|
||||||
|
help="Manage dotfile symlinks",
|
||||||
|
no_args_is_help=False,
|
||||||
|
context_settings=_HELP_CONTEXT,
|
||||||
|
)
|
||||||
|
dotfiles_repos_app = typer.Typer(
|
||||||
|
cls=_AliasGroupingTyperGroup,
|
||||||
|
help="Manage dotfiles and module repos",
|
||||||
|
no_args_is_help=False,
|
||||||
|
context_settings=_HELP_CONTEXT,
|
||||||
|
)
|
||||||
|
packages_app = typer.Typer(
|
||||||
|
cls=_AliasGroupingTyperGroup,
|
||||||
|
help="Manage packages",
|
||||||
|
no_args_is_help=False,
|
||||||
|
context_settings=_HELP_CONTEXT,
|
||||||
|
)
|
||||||
|
setup_app = typer.Typer(
|
||||||
|
cls=_AliasGroupingTyperGroup,
|
||||||
|
help="Bootstrap a system profile",
|
||||||
|
no_args_is_help=False,
|
||||||
|
context_settings=_HELP_CONTEXT,
|
||||||
|
)
|
||||||
|
remote_app = typer.Typer(
|
||||||
|
cls=_AliasGroupingTyperGroup,
|
||||||
|
help="Manage remote targets",
|
||||||
|
no_args_is_help=False,
|
||||||
|
context_settings=_HELP_CONTEXT,
|
||||||
|
)
|
||||||
|
dev_app = typer.Typer(
|
||||||
|
cls=_AliasGroupingTyperGroup,
|
||||||
|
help="Manage development containers",
|
||||||
|
no_args_is_help=False,
|
||||||
|
context_settings=_HELP_CONTEXT,
|
||||||
|
)
|
||||||
|
projects_app = typer.Typer(
|
||||||
|
cls=_AliasGroupingTyperGroup,
|
||||||
|
help="Manage git projects",
|
||||||
|
no_args_is_help=False,
|
||||||
|
context_settings=_HELP_CONTEXT,
|
||||||
|
)
|
||||||
|
completion_app = typer.Typer(
|
||||||
|
cls=_AliasGroupingTyperGroup,
|
||||||
|
help="Shell completion helpers",
|
||||||
|
no_args_is_help=False,
|
||||||
|
context_settings=_HELP_CONTEXT,
|
||||||
)
|
)
|
||||||
dotfiles_app = typer.Typer(help="Manage dotfile symlinks", no_args_is_help=False)
|
|
||||||
dotfiles_repos_app = typer.Typer(help="Manage dotfiles and module repos", no_args_is_help=False)
|
|
||||||
packages_app = typer.Typer(help="Manage packages", no_args_is_help=False)
|
|
||||||
setup_app = typer.Typer(help="Bootstrap a system profile", no_args_is_help=False)
|
|
||||||
remote_app = typer.Typer(help="Manage remote targets", no_args_is_help=False)
|
|
||||||
dev_app = typer.Typer(help="Manage development containers", no_args_is_help=False)
|
|
||||||
projects_app = typer.Typer(help="Manage git projects", no_args_is_help=False)
|
|
||||||
completion_app = typer.Typer(help="Shell completion helpers", no_args_is_help=False)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_callback(value: bool) -> None:
|
def _version_callback(value: bool) -> None:
|
||||||
@@ -52,6 +148,7 @@ def _root(
|
|||||||
version: bool = typer.Option(
|
version: bool = typer.Option(
|
||||||
False,
|
False,
|
||||||
"--version",
|
"--version",
|
||||||
|
"-v",
|
||||||
help="Show version",
|
help="Show version",
|
||||||
callback=_version_callback,
|
callback=_version_callback,
|
||||||
is_eager=True,
|
is_eager=True,
|
||||||
@@ -342,20 +439,6 @@ def remote_list(ctx: typer.Context) -> None:
|
|||||||
_run(ctx, lambda flow_ctx: RemoteService(flow_ctx).list())
|
_run(ctx, lambda flow_ctx: RemoteService(flow_ctx).list())
|
||||||
|
|
||||||
|
|
||||||
@app.command("enter")
|
|
||||||
def enter_alias(
|
|
||||||
ctx: typer.Context,
|
|
||||||
target: str = typer.Argument(..., help="Target ([user@]namespace@platform)"),
|
|
||||||
user: Optional[str] = typer.Option(None, "--user", "-u", help="SSH user override"),
|
|
||||||
namespace: Optional[str] = typer.Option(None, "--namespace", "-n", help="Namespace override"),
|
|
||||||
platform: Optional[str] = typer.Option(None, "--platform", "-p", help="Platform override"),
|
|
||||||
session: Optional[str] = typer.Option(None, "--session", "-s", help="tmux session name"),
|
|
||||||
no_tmux: bool = typer.Option(False, "--no-tmux", help="Open plain SSH without tmux"),
|
|
||||||
dry_run: bool = typer.Option(False, "--dry-run", "-d"),
|
|
||||||
) -> None:
|
|
||||||
_remote_enter(ctx, target, user, namespace, platform, session, no_tmux, dry_run)
|
|
||||||
|
|
||||||
|
|
||||||
@dev_app.callback(invoke_without_command=True)
|
@dev_app.callback(invoke_without_command=True)
|
||||||
def dev_default(ctx: typer.Context) -> None:
|
def dev_default(ctx: typer.Context) -> None:
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
@@ -462,12 +545,9 @@ def projects_summary(ctx: typer.Context) -> None:
|
|||||||
_run(ctx, lambda flow_ctx: ProjectService(flow_ctx).summary())
|
_run(ctx, lambda flow_ctx: ProjectService(flow_ctx).summary())
|
||||||
|
|
||||||
|
|
||||||
@app.command("sync")
|
@projects_app.command("sync")
|
||||||
def sync_alias(
|
def projects_sync(ctx: typer.Context) -> None:
|
||||||
ctx: typer.Context,
|
_projects_check(ctx, fetch=True)
|
||||||
fetch: bool = typer.Option(True, "--fetch/--no-fetch", help="Fetch remotes first"),
|
|
||||||
) -> None:
|
|
||||||
_projects_check(ctx, fetch)
|
|
||||||
|
|
||||||
|
|
||||||
@completion_app.callback(invoke_without_command=True)
|
@completion_app.callback(invoke_without_command=True)
|
||||||
|
|||||||
@@ -40,6 +40,38 @@ def test_help_flag():
|
|||||||
assert "setup" in result.stdout
|
assert "setup" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_short_help_flag():
|
||||||
|
"""Test -h shows commands."""
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "flow", "-h"],
|
||||||
|
capture_output=True, text=True,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "dotfiles" in result.stdout
|
||||||
|
assert "packages" in result.stdout
|
||||||
|
assert "setup" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_help_groups_aliases():
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "flow", "--help"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
lines = [line for line in result.stdout.splitlines() if "│" in line]
|
||||||
|
|
||||||
|
dotfiles_line = next(line for line in lines if "dotfiles" in line and "dot" in line)
|
||||||
|
assert "dotfiles, dot" in dotfiles_line
|
||||||
|
assert next(line for line in lines if "packages" in line and "package" in line).startswith("│")
|
||||||
|
packages_line = next(line for line in lines if "packages" in line and "pkg" in line)
|
||||||
|
assert "packages, package, pkg" in packages_line
|
||||||
|
setup_line = next(line for line in lines if "setup" in line and "bootstrap" in line)
|
||||||
|
assert "setup, bootstrap, provision" in setup_line
|
||||||
|
projects_line = next(line for line in lines if "projects" in line and "project" in line)
|
||||||
|
assert "projects, project" in projects_line
|
||||||
|
|
||||||
|
|
||||||
def test_no_color_flag():
|
def test_no_color_flag():
|
||||||
"""Test --no-color flag is accepted."""
|
"""Test --no-color flag is accepted."""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -64,6 +96,10 @@ def test_dotfiles_help():
|
|||||||
assert "sync" not in result.stdout
|
assert "sync" not in result.stdout
|
||||||
assert "modules" not in result.stdout
|
assert "modules" not in result.stdout
|
||||||
|
|
||||||
|
repos_lines = [line for line in result.stdout.splitlines() if "repos" in line]
|
||||||
|
assert len(repos_lines) == 1
|
||||||
|
assert "repos, repo" in repos_lines[0]
|
||||||
|
|
||||||
|
|
||||||
def test_packages_help():
|
def test_packages_help():
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
|
|||||||
Reference in New Issue
Block a user