Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions lib/devbase/tui/actions_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,15 +211,8 @@ def _select_scoped_project(devbase_root: Path, message: str, choices):
# 各操作の引数収集 + dispatch (plan 2.3 契約)
# ---------------------------------------------------------------------------

def _op_init(devbase_root: Path):
# 既存設定がある場合は --reset でやり直し (既存はバックアップされる)。
reset = flow.need(menu.confirm(
"既存の設定をバックアップしてやり直しますか (--reset)?", default=False))
return _dispatch(devbase_root, "init", reset=reset)


def _op_list(devbase_root: Path):
"""``env list``: 表示範囲 + 表示オプションを収集して一覧表示する
"""``env list``: 表示範囲を収集して一覧表示する

ハンドラ (``cmd_env_list``) は CWD (PWD) が projects/ 配下のときだけ
プロジェクト .env を表示するため、プロジェクトを含む表示範囲
Expand All @@ -233,13 +226,12 @@ def _op_list(devbase_root: Path):
[("グローバル + プロジェクト", "both"),
("グローバルのみ (--global)", "global"),
("プロジェクトのみ (--project)", "project")])
reveal = flow.need(menu.confirm(
"機密値を伏せ字にせず表示しますか (--reveal)?", default=False))
keys_only = flow.need(menu.confirm("キー名のみ表示しますか (--keys)?", default=False))

# --reveal / --keys は CLI 既定 (False = 機密値は伏せ字・通常表示) で実行する
# (非破壊操作の確認プロンプト廃止)。必要な場合は CLI を使う想定。
attrs = {"global_only": scope == "global",
"project_only": scope == "project",
"reveal": reveal, "keys_only": keys_only}
"reveal": False, "keys_only": False}
if name is None:
return _dispatch(devbase_root, "list", **attrs)
return _run_in_project(devbase_root, name,
Expand Down Expand Up @@ -321,9 +313,11 @@ def _op_import(devbase_root: Path):
# sync は引数なしで即実行 (ソースファイルから認証情報を再同期する)。
# edit も引数なし。$DEVBASE_ROOT/.env を $EDITOR で開くグローバル操作のため
# chdir しない (plan 3.3 は CWD スコープとするが実装を正とする)。
# init は --reset なし (CLI 既定) で即実行。セットアップ済みなら
# cmd_env_init が案内を出して安全に終了し、やり直しは CLI --reset を使う。
"sync": lambda root: _dispatch(root, "sync"),
"edit": lambda root: _dispatch(root, "edit"),
"init": _op_init,
"init": lambda root: _dispatch(root, "init", reset=False),
"list": _op_list,
"set": _op_set,
"get": _op_get,
Expand Down
25 changes: 10 additions & 15 deletions lib/devbase/tui/actions_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@

# plugin サブコマンド (表示順 = ハイライト既定順)。閲覧系の list を先頭に置き、
# Enter 連打で安全な一覧表示へ到達できるようにする。value は cmd_plugin の subcommand 名
# (repo のみサブ階層メニューへの分岐)。
# (repo のみサブ階層メニューへの分岐)。--available は y/N で聞かず独立した
# メニュー項目にする (非破壊操作の確認プロンプト廃止)。
_PLUGIN_OPS: list[tuple[str, str]] = [
("一覧表示 (list)", "list"),
("導入済み一覧 (list)", "list"),
("利用可能一覧 (list --available)", "list-available"),
("インストール (install)", "install"),
("アンインストール (uninstall)", "uninstall"),
("更新 (update)", "update"),
Expand Down Expand Up @@ -149,22 +151,13 @@ def _select_repo_operation():
# 各操作の引数収集 + dispatch (plan 2.3 契約)
# ---------------------------------------------------------------------------

def _op_list(devbase_root: Path):
# --available: 導入済み一覧の代わりに未導入の利用可能 plugin を表示する。
available = flow.need(menu.confirm(
"未導入の利用可能 plugin を表示しますか (--available)?", default=False))
return _dispatch(devbase_root, "list", available=available)


def _op_install(devbase_root: Path):
# --link / --all は CLI 既定 (False) で実行する。指定が必要な場合は CLI
# (`plugin install --link/--all`) を使う想定 (非破壊操作の確認プロンプト廃止)。
source = flow.need(menu.text(
"インストールする plugin の source (名前 / URL / パス)", allow_empty=False))
link = flow.need(menu.confirm(
"symlink としてインストールしますか (--link)?", default=False))
install_all = flow.need(menu.confirm(
"リポジトリ内の全 plugin をインストールしますか (--all)?", default=False))
return _dispatch(devbase_root, "install",
source=source, link=link, install_all=install_all)
source=source, link=False, install_all=False)


def _op_uninstall(devbase_root: Path):
Expand All @@ -188,7 +181,9 @@ def _op_info(devbase_root: Path):


_OP_HANDLERS = {
"list": _op_list,
# list 系は引数収集なしで即実行 (--available はメニュー項目で分岐)。
"list": lambda root: _dispatch(root, "list", available=False),
"list-available": lambda root: _dispatch(root, "list", available=True),
"install": _op_install,
"uninstall": _op_uninstall,
"update": _op_update,
Expand Down
30 changes: 11 additions & 19 deletions lib/devbase/tui/actions_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
PR2 で running 操作サブメニューを **up/down/login/ps/logs/scale/build/rebuild の全操作**
へ拡張した。login/ps/logs/scale は running 中コンテナを対象とするため running 行限定、
stopped/unknown は従来どおり直接 up (PR1 非回帰)。引数を要する操作は ``tui.menu`` の
収集ヘルパで CLI と同じ属性値を集め、破壊的な down は実行前に確認する
(plan 2.3 契約表 / 3.4 破壊的操作確認)
収集ヘルパで CLI と同じ属性値を集める (plan 2.3 契約表)。down はデータを失わない
(volume 保持) ためメニュー選択を意思表示とみなし、確認プロンプトは出さない

プロジェクト一覧の表示・選択は ``tui.app`` (トップ画面) が担い、本モジュールは
選択された 1 行の処理 (``handle_row``) と questionary 不在時のフォールバックを提供する。
Expand Down Expand Up @@ -93,11 +93,6 @@ def _select_build_image(devbase_root: Path):
# 各操作の引数収集 + dispatch (引数を要する操作のみ。up/rebuild は即実行)
# ---------------------------------------------------------------------------

def _op_down(devbase_root: Path, name: str):
flow.confirm_or_back(f"'{name}' のコンテナを停止しますか?")
return dispatch_lifecycle("down", name)


def _op_login(devbase_root: Path, name: str):
# menu.text は空入力 (既定値を消して確定) で "" を返し、wrapper で --index=
# と展開されてコマンドが失敗する。menu.integer なら空入力は default=1 を返し、
Expand All @@ -106,16 +101,11 @@ def _op_login(devbase_root: Path, name: str):
return dispatch_lifecycle("login", name, index=str(index))


def _op_ps(devbase_root: Path, name: str):
all_c = flow.need(menu.confirm(
"停止中も含め全コンテナを表示しますか (--all)?", default=False))
return dispatch_lifecycle("ps", name, all=all_c)


def _op_logs(devbase_root: Path, name: str):
follow = flow.need(menu.confirm("ログを追従表示しますか (--follow)?", default=False))
# --follow は CLI 既定 (False) で実行する (非破壊操作の確認プロンプト廃止)。
# 追従が必要な場合は CLI (`project logs --follow`) を使う想定。
tail = flow.need_optional(_optional_int("末尾何行を表示しますか (空で全件)"))
return dispatch_lifecycle("logs", name, follow=follow, tail=tail)
return dispatch_lifecycle("logs", name, follow=False, tail=tail)


def _op_scale(devbase_root: Path, name: str):
Expand All @@ -129,13 +119,15 @@ def _op_build(devbase_root: Path, name: str):


_OP_HANDLERS = {
# up/rebuild は引数なしで即実行。up は scale 属性を参照する (常に None。
# 他コマンドは無視する)。
# up/down/rebuild/ps は引数なしで即実行。up/rebuild は scale 属性を参照する
# (常に None。他コマンドは無視する)。down はデータを失わない (volume 保持・
# up で復旧可能) ためメニュー選択を意思表示とみなし、確認プロンプトを出さない。
# ps の --all は CLI 既定 (False) に揃える。
"up": lambda root, name: dispatch_lifecycle("up", name, scale=None),
"rebuild": lambda root, name: dispatch_lifecycle("rebuild", name, scale=None),
"down": _op_down,
"down": lambda root, name: dispatch_lifecycle("down", name),
"login": _op_login,
"ps": _op_ps,
"ps": lambda root, name: dispatch_lifecycle("ps", name, all=False),
"logs": _op_logs,
"scale": _op_scale,
"build": _op_build,
Expand Down
6 changes: 3 additions & 3 deletions lib/devbase/tui/actions_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ def _optional_point(message: str):
# ---------------------------------------------------------------------------

def _op_create(devbase_root: Path):
# --full は CLI 既定 (False = 増分) で実行する (非破壊操作の確認プロンプト
# 廃止)。フルバックアップ強制は CLI (`snapshot create --full`) を使う想定。
name = flow.need(menu.text("スナップショット名 (空でタイムスタンプ自動命名)",
allow_empty=True))
full = flow.need(menu.confirm("フルバックアップを強制しますか (--full)?",
default=False))
# 空入力は CLI の --name 省略と同じ None (自動命名) に正規化する。
return dispatch_group(cmd_snapshot, devbase_root, "create",
name=name or None, full=full)
name=name or None, full=False)


def _op_restore(devbase_root: Path):
Expand Down
61 changes: 16 additions & 45 deletions tests/cli/tui/test_actions_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,30 +166,21 @@ def test_run_operation_edit_is_global_no_project_select(monkeypatch, tmp_path):
assert captured["cwd"] == before, "edit は chdir しない"


@pytest.mark.parametrize("reset", [True, False])
def test_run_operation_init_collects_reset(monkeypatch, tmp_path, reset):
"""init は confirm の結果を --reset として渡す (plan 2.3: reset 既定 False)。"""
def test_run_operation_init_runs_without_confirm(monkeypatch, tmp_path):
"""init は確認プロンプトなしで reset=False (CLI 既定) のまま即実行する。

セットアップ済みの環境では cmd_env_init が案内を出して安全に終了する。
やり直しは CLI (`env init --reset`) を使う想定。
"""
captured = _capture_dispatch(monkeypatch)
monkeypatch.setattr(menu, "confirm", lambda *a, **k: reset)
monkeypatch.setattr(menu, "confirm",
lambda *a, **k: pytest.fail("init で確認を求めない"))
assert actions_env._run_operation(tmp_path, "init") == 0
assert captured["attrs"] == {"subcommand": "init", "reset": reset}


@pytest.mark.parametrize("confirm_ret", ["BACK", None])
def test_run_operation_init_cancel(monkeypatch, tmp_path, confirm_ret):
"""init の confirm で Esc は再表示 (_ARG_CANCEL)、Ctrl-C は全体中止 (None)。"""
from devbase.commands import env as env_mod
called = []
monkeypatch.setattr(env_mod, "cmd_env", lambda root, args: called.append(1) or 0)
ret = menu.MENU_BACK if confirm_ret == "BACK" else None
monkeypatch.setattr(menu, "confirm", lambda *a, **k: ret)
expected = actions_env._ARG_CANCEL if confirm_ret == "BACK" else None
assert actions_env._run_operation(tmp_path, "init") is expected
assert called == []
assert captured["attrs"] == {"subcommand": "init", "reset": False}


# ---------------------------------------------------------------------------
# _run_operation: list (表示範囲 + reveal/keys)
# _run_operation: list (表示範囲のみ収集。reveal/keys は CLI 既定の False)
# ---------------------------------------------------------------------------

def test_run_operation_list_global_scope_no_chdir(monkeypatch, tmp_path):
Expand All @@ -198,15 +189,15 @@ def test_run_operation_list_global_scope_no_chdir(monkeypatch, tmp_path):
monkeypatch.setattr(menu, "select", lambda *a, **k: "global")
monkeypatch.setattr(actions_env, "_select_project",
lambda root: pytest.fail("global でプロジェクト選択してはいけない"))
confirms = iter([True, False]) # reveal=True, keys_only=False
monkeypatch.setattr(menu, "confirm", lambda *a, **k: next(confirms))
monkeypatch.setattr(menu, "confirm",
lambda *a, **k: pytest.fail("list で確認を求めない"))

before = os.getcwd()
assert actions_env._run_operation(tmp_path, "list") == 0
assert captured["attrs"] == {
"subcommand": "list",
"global_only": True, "project_only": False,
"reveal": True, "keys_only": False,
"reveal": False, "keys_only": False,
}
assert captured["cwd"] == before, "global スコープは chdir しない"

Expand All @@ -223,16 +214,14 @@ def test_run_operation_list_both_scope_chdirs_and_restores(monkeypatch, tmp_path
target.mkdir(parents=True)
monkeypatch.setattr(menu, "select", lambda *a, **k: "both")
monkeypatch.setattr(actions_env, "_select_project", lambda root: "carmo")
confirms = iter([True, False]) # reveal=True, keys_only=False
monkeypatch.setattr(menu, "confirm", lambda *a, **k: next(confirms))
monkeypatch.setenv("PWD", str(tmp_path))

before = os.getcwd()
assert actions_env._run_operation(tmp_path, "list") == 0
assert captured["attrs"] == {
"subcommand": "list",
"global_only": False, "project_only": False,
"reveal": True, "keys_only": False,
"reveal": False, "keys_only": False,
}
# ハンドラ実行中は projects/carmo に居る (グローバル + プロジェクト両方が出る)
assert captured["cwd"] == str(target)
Expand All @@ -253,16 +242,14 @@ def test_run_operation_list_project_chdirs_and_restores(monkeypatch, tmp_path):
target.mkdir(parents=True)
monkeypatch.setattr(menu, "select", lambda *a, **k: "project")
monkeypatch.setattr(actions_env, "_select_project", lambda root: "carmo")
confirms = iter([False, True]) # reveal=False, keys_only=True
monkeypatch.setattr(menu, "confirm", lambda *a, **k: next(confirms))
monkeypatch.setenv("PWD", str(tmp_path))

before = os.getcwd()
assert actions_env._run_operation(tmp_path, "list") == 0
assert captured["attrs"] == {
"subcommand": "list",
"global_only": False, "project_only": True,
"reveal": False, "keys_only": True,
"reveal": False, "keys_only": False,
}
# ハンドラ実行中は projects/carmo に居る (CWD と PWD の両方を切り替える)
assert captured["cwd"] == str(target)
Expand All @@ -273,15 +260,13 @@ def test_run_operation_list_project_chdirs_and_restores(monkeypatch, tmp_path):


def test_run_operation_list_project_select_cancel(monkeypatch, tmp_path):
"""list のプロジェクト選択を中止したら表示オプション収集にも進まない。"""
"""list のプロジェクト選択を中止したら実行しない。"""
from devbase.commands import env as env_mod
called = []
monkeypatch.setattr(env_mod, "cmd_env", lambda root, args: called.append(1) or 0)
monkeypatch.setattr(menu, "select", lambda *a, **k: "project")
monkeypatch.setattr(actions_env, "_select_project",
lambda root: actions_env._ARG_CANCEL)
monkeypatch.setattr(menu, "confirm",
lambda *a, **k: pytest.fail("選択中止後に確認を求めない"))
assert actions_env._run_operation(tmp_path, "list") is actions_env._ARG_CANCEL
assert called == []

Expand All @@ -299,20 +284,6 @@ def test_run_operation_list_scope_cancel(monkeypatch, tmp_path, scope_ret):
assert called == []


@pytest.mark.parametrize("confirm_ret", ["BACK", None])
def test_run_operation_list_confirm_cancel(monkeypatch, tmp_path, confirm_ret):
"""reveal の confirm で Esc は再表示 (_ARG_CANCEL)、Ctrl-C は全体中止 (None)。"""
from devbase.commands import env as env_mod
called = []
monkeypatch.setattr(env_mod, "cmd_env", lambda root, args: called.append(1) or 0)
monkeypatch.setattr(menu, "select", lambda *a, **k: "global")
ret = menu.MENU_BACK if confirm_ret == "BACK" else None
monkeypatch.setattr(menu, "confirm", lambda *a, **k: ret)
expected = actions_env._ARG_CANCEL if confirm_ret == "BACK" else None
assert actions_env._run_operation(tmp_path, "list") is expected
assert called == []


# ---------------------------------------------------------------------------
# _run_operation: get / delete
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading