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
|
||||
BINARY := $(DIST_DIR)/flow
|
||||
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:
|
||||
@printf "Targets:\n"
|
||||
@@ -16,6 +19,7 @@ help:
|
||||
@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 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 install Build and install binary to ~/.local/bin/flow\n"
|
||||
@printf " make check-binary Run dist/flow --help\n"
|
||||
@@ -38,6 +42,16 @@ check: test
|
||||
package: deps
|
||||
$(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
|
||||
$(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
|
||||
|
||||
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
|
||||
uv sync --locked --extra dev --extra build
|
||||
@@ -23,13 +48,6 @@ flow setup show linux-work
|
||||
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:
|
||||
|
||||
```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
|
||||
are ignored. `.venv/` is the uv-managed local environment. `venv/` is legacy
|
||||
local clutter and should not be used.
|
||||
are ignored. `.venv/` and `venv/` are local virtual environments and are also
|
||||
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
|
||||
|
||||
@@ -71,7 +123,6 @@ flow setup run [PROFILE|--profile NAME] [--dry-run] [--var KEY=VALUE]
|
||||
# Remote targets
|
||||
flow remote list
|
||||
flow remote enter TARGET [--user USER] [--namespace NAME] [--platform NAME] [--session NAME] [--no-tmux] [--dry-run]
|
||||
flow enter TARGET
|
||||
|
||||
# Dev containers
|
||||
flow dev create NAME --image IMAGE [--project PATH] [--dry-run]
|
||||
@@ -87,7 +138,7 @@ flow dev list
|
||||
flow projects check [--fetch]
|
||||
flow projects fetch
|
||||
flow projects summary
|
||||
flow sync
|
||||
flow projects sync
|
||||
|
||||
# Shell completion
|
||||
flow completion zsh
|
||||
@@ -104,9 +155,8 @@ Aliases:
|
||||
- `dotfiles` -> `dot`
|
||||
- `dotfiles repos` -> `dotfiles repo`
|
||||
- `packages` -> `package`, `pkg`
|
||||
- `projects` -> `project`; `flow sync` -> `flow projects check --fetch`
|
||||
- `projects` -> `project`
|
||||
- `setup` -> `bootstrap`, `provision`
|
||||
- `remote enter` -> `enter`
|
||||
- `dev attach` -> `dev connect`
|
||||
- `dev remove` -> `dev rm`
|
||||
|
||||
@@ -240,6 +290,7 @@ make test # unit tests, excluding e2e
|
||||
make test-e2e # requires Docker or healthy Podman
|
||||
make check # tests plus CLI smoke checks
|
||||
make package # wheel and sdist in dist/
|
||||
make release-package # installable GitHub Release tarball
|
||||
make build # PyInstaller binary in dist/flow
|
||||
make clean # remove build/test artifacts
|
||||
make distclean # also remove .venv/ and venv/
|
||||
|
||||
@@ -68,10 +68,8 @@ flow dotfiles repos list|status|pull|push
|
||||
flow packages install|remove|list
|
||||
flow setup run|show|list
|
||||
flow remote enter|list
|
||||
flow enter
|
||||
flow dev create|attach|connect|exec|enter|stop|remove|rm|respawn|list
|
||||
flow projects check|fetch|summary
|
||||
flow sync
|
||||
flow projects check|fetch|summary|sync
|
||||
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",
|
||||
"pkg": "packages",
|
||||
"project": "projects",
|
||||
"sync": "projects",
|
||||
}
|
||||
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]:
|
||||
if len(before) <= 1:
|
||||
return _filter(["check", "fetch", "summary"], current)
|
||||
return _filter(["check", "fetch", "summary", "sync"], current)
|
||||
if before[1] == "check" and current.startswith("-"):
|
||||
return _filter(["--fetch", "--no-fetch"], current)
|
||||
return []
|
||||
|
||||
136
src/flow/cli.py
136
src/flow/cli.py
@@ -2,11 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
from typer.main import TyperGroup
|
||||
|
||||
from flow import __version__
|
||||
from flow.core import paths
|
||||
@@ -23,21 +26,114 @@ from flow.app.projects import ProjectService
|
||||
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(
|
||||
cls=_AliasGroupingTyperGroup,
|
||||
name="flow",
|
||||
help="DevFlow - development environment manager",
|
||||
no_args_is_help=True,
|
||||
add_completion=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:
|
||||
@@ -52,6 +148,7 @@ def _root(
|
||||
version: bool = typer.Option(
|
||||
False,
|
||||
"--version",
|
||||
"-v",
|
||||
help="Show version",
|
||||
callback=_version_callback,
|
||||
is_eager=True,
|
||||
@@ -342,20 +439,6 @@ def remote_list(ctx: typer.Context) -> None:
|
||||
_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)
|
||||
def dev_default(ctx: typer.Context) -> 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())
|
||||
|
||||
|
||||
@app.command("sync")
|
||||
def sync_alias(
|
||||
ctx: typer.Context,
|
||||
fetch: bool = typer.Option(True, "--fetch/--no-fetch", help="Fetch remotes first"),
|
||||
) -> None:
|
||||
_projects_check(ctx, fetch)
|
||||
@projects_app.command("sync")
|
||||
def projects_sync(ctx: typer.Context) -> None:
|
||||
_projects_check(ctx, fetch=True)
|
||||
|
||||
|
||||
@completion_app.callback(invoke_without_command=True)
|
||||
|
||||
@@ -40,6 +40,38 @@ def test_help_flag():
|
||||
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():
|
||||
"""Test --no-color flag is accepted."""
|
||||
result = subprocess.run(
|
||||
@@ -64,6 +96,10 @@ def test_dotfiles_help():
|
||||
assert "sync" 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():
|
||||
result = subprocess.run(
|
||||
|
||||
Reference in New Issue
Block a user