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
10 changes: 6 additions & 4 deletions containers/base/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg; \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list; \
# NodeSource リポジトリ
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -; \
# NodeSource リポジトリ (Active LTS を追従。バージョン固定しない)
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -; \
# ブラウザ: amd64はChrome+Chromium、arm64はChromium
arch="$(dpkg --print-architecture)"; \
BROWSER_PKG=""; \
Expand Down Expand Up @@ -144,8 +144,10 @@ RUN set -eux; \
fi; \
# Playwright browsers
npx playwright install --with-deps chromium; \
# クリーンアップ
rm -rf /tmp/* \
# クリーンアップ。--with-deps の sudo apt や Node24/npm11 の
# /tmp/node-compile-cache が root 所有ファイルを残すため sudo で削除する
# (非 root の rm では Permission denied になりビルドが失敗する)。
sudo rm -rf /tmp/* \
~/.cache \
~/.npm \
~/.config/yarn \
Expand Down
8 changes: 3 additions & 5 deletions lib/devbase/tui/actions_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,11 @@ def run(devbase_root: Path):
"""環境変数カテゴリのエントリ。操作選択 → 引数収集 → cmd_env へ委譲。

戻り値プロトコル (``flow.menu_loop``。トップループが ``is`` 同一性で判定する):
- **操作を実行した場合**: dispatch の rc (``int``) を返す。「実行したのでトップへ
戻る、rc は呼び出し側が記憶」の意味で、失敗が ``devbase list`` の終了コードへ
伝搬する。
- ``menu.MENU_BACK``: サブメニューで Esc/← (操作なしでトップへ)。
- ``menu.MENU_BACK``: サブメニューで Esc/← (トップへ戻る)。操作を実行しても
(出力確認の一時停止後) サブメニューに留まり、Esc/← で初めてトップへ戻る。
- ``None``: Ctrl-C による全体中止。

引数収集を中止 (``_ARG_CANCEL``) した場合はサブメニューを再表示する
操作実行後・引数収集中止 (``_ARG_CANCEL``) のいずれもサブメニューを再表示する
"""
return flow.menu_loop(_select_action,
lambda op: _run_operation(devbase_root, op))
21 changes: 11 additions & 10 deletions lib/devbase/tui/actions_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,9 @@ def _run_repo_operation(devbase_root: Path, op: str):
def _repo_menu(devbase_root: Path):
"""plugin repo のサブ階層メニューを回す。

戻り値 (``flow.menu_loop`` のプロトコル): dispatch の rc (``int``) /
``menu.MENU_BACK`` (Esc・← で plugin メニューへ戻る) / ``None`` (Ctrl-C 全体中止)。
引数収集を中止 (``_ARG_CANCEL``) した場合はサブ階層メニューを再表示する
戻り値 (``flow.menu_loop`` のプロトコル): ``menu.MENU_BACK`` (Esc・← で plugin
メニューへ戻る) / ``None`` (Ctrl-C 全体中止)。操作実行後は (出力確認の一時停止
後) サブ階層メニューに留まり、引数収集を中止 (``_ARG_CANCEL``) した場合も再表示する
"""
return flow.menu_loop(_select_repo_operation,
lambda op: _run_repo_operation(devbase_root, op))
Expand All @@ -281,19 +281,20 @@ def run(devbase_root: Path):
"""プラグイン操作カテゴリ。操作選択 → 引数収集 → ``cmd_plugin`` へ委譲。

戻り値プロトコル (トップループが ``is`` 同一性で判定する。actions_project と同じ):
- **操作を実行した場合**: dispatch の rc (``int``) を返す。失敗 (非0) は
``devbase list`` の終了コードへ伝搬する。
- ``menu.MENU_BACK``: 操作なしでトップメニューへ戻る (Esc/←)
- ``menu.MENU_BACK``: plugin メニューで Esc/← (トップメニューへ戻る)。操作を
実行しても (出力確認の一時停止後) plugin メニューに留まり、Esc/← で初めて
トップへ戻る
- ``None``: Ctrl-C による全体中止。

repo はサブ階層メニュー (``_repo_menu``) へ分岐し、Esc/← で plugin メニューへ
戻れる (``MENU_BACK````_ARG_CANCEL`` 相当に読み替えて再表示する)。
操作完了後はトップメニューへ復帰する (plan 3.5 状態遷移: Exec → Top)。
repo はサブ階層メニュー (``_repo_menu``) へ分岐する。repo 側も実行後はそこに
留まり、Esc/← (``MENU_BACK``) で plugin メニューへ戻る (``_ARG_CANCEL`` 相当に
読み替えて再表示する)。
"""
def _run(op):
if op == "repo":
rc = _repo_menu(devbase_root)
# repo 階層から Esc で戻ったら plugin メニューを再表示する。
# repo 階層は自身で操作を完結する。Esc/← (MENU_BACK) で戻ったら plugin
# メニューを再表示し、Ctrl-C (None) はそのまま全体中止へ伝搬する。
return _ARG_CANCEL if rc is menu.MENU_BACK else rc
return _run_operation(devbase_root, op)

Expand Down
44 changes: 30 additions & 14 deletions lib/devbase/tui/actions_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
("再ビルド (rebuild --no-cache)", "rebuild"),
]

# 実行後にサブメニューへ留まらずトップ一覧へ戻る操作。up/down はコンテナの
# 起動状態が大きく変わるため、最新状態の一覧を見せる方が自然 (それ以外の
# login/ps/logs/scale/build/rebuild は連続操作できるようサブメニューに留まる)。
_BACK_TO_TOP_OPS = frozenset({"up", "down"})

# 中止系番兵は flow と同一オブジェクトを再公開する (呼び出し側・テストの契約)。
_ARG_CANCEL = flow.ARG_CANCEL
_ABORT = flow.ABORT
Expand Down Expand Up @@ -119,12 +124,16 @@ def _op_build(devbase_root: Path, name: str):


_OP_HANDLERS = {
# up/down/rebuild/ps は引数なしで即実行。up/rebuild は scale 属性を参照する
# (常に None。他コマンドは無視する)。down はデータを失わない (volume 保持・
# up で復旧可能) ためメニュー選択を意思表示とみなし、確認プロンプトを出さない。
# up/down/ps は引数なしで即実行。up は 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),
# 「再ビルド (rebuild --no-cache)」はラベル通り base/project とも無条件 no-cache
# で再ビルドする。CLI の `rebuild` は期限判定 (= build --expires=7) でキャッシュ
# を使い、新しいイメージはスキップするため --no-cache を満たさない。no-cache の
# 正規経路は `build --no-cache` なので build に no_cache=True を渡して委譲する。
"rebuild": lambda root, name: dispatch_lifecycle("build", name, no_cache=True),
"down": lambda root, name: dispatch_lifecycle("down", name),
"login": _op_login,
"ps": lambda root, name: dispatch_lifecycle("ps", name, all=False),
Expand Down Expand Up @@ -152,27 +161,34 @@ def _run_operation(devbase_root: Path, name: str, op: str):
def _operation_menu(devbase_root: Path, name: str):
"""running 行の操作サブメニューを回す。

戻り値 (``flow.menu_loop`` のプロトコル): dispatch の rc (``int``) /
``menu.MENU_BACK`` (Esc・← で一覧へ戻る) / ``None`` (Ctrl-C 全体中止)。
引数収集を中止 (``_ARG_CANCEL``) した場合はサブメニューを再表示する。
戻り値 (``flow.menu_loop`` のプロトコル): ``menu.MENU_BACK`` (Esc・← で一覧へ
戻る) / ``None`` (Ctrl-C 全体中止)。多くの操作は実行後もトップ一覧へ戻らず、
出力を読めるよう一時停止してから**同じサブメニューを再表示する** (留まる)。
ただし up/down (``_BACK_TO_TOP_OPS``) は状態が大きく変わるため実行後はトップ
一覧へ戻り最新状態を再表示する。引数収集を中止 (``_ARG_CANCEL``) した場合も
同じサブメニューを再表示する。
"""
return flow.menu_loop(
lambda: _select_action(name),
lambda op: _run_operation(devbase_root, name, op))
lambda op: _run_operation(devbase_root, name, op),
back_after=lambda op: op in _BACK_TO_TOP_OPS)


def handle_row(devbase_root: Path, row: dict):
"""一覧で選択された 1 プロジェクト行を処理する (トップ画面から呼ばれる)。

戻り値プロトコル (トップループが ``is`` 同一性で判定する):
- **操作を実行した場合**: ``dispatch_lifecycle`` の rc (``int``) を返す。
「実行したので一覧へ戻る、rc は呼び出し側が記憶」の意味。これにより
project 操作の失敗が ``devbase list`` の終了コードへ伝搬する。
- ``menu.MENU_BACK``: 操作サブメニューで Esc/← (操作なしで一覧へ)。
- ``menu.MENU_BACK``: 操作サブメニューで Esc/← (一覧へ戻る)。running 行は操作を
実行しても (出力確認の一時停止後) サブメニューに留まり、Esc/← で初めて一覧へ
戻るため、この値を返す。
- **直接 up を実行した場合** (stopped / unknown 行): ``dispatch_lifecycle`` の
rc (``int``) を返す。トップループが rc を記憶し ``devbase list`` の終了コードへ
伝搬する。
- ``None``: サブメニューで Ctrl-C による全体中止。

選択行が running 中なら ``_operation_menu`` で全操作を選ばせ、それ以外
(stopped / unknown) は従来どおり直接 ``project up`` を起動する (PR1 非回帰)。
選択行が running 中なら ``_operation_menu`` で全操作を選ばせ (実行後もサブ
メニューに留まる)、それ以外 (stopped / unknown) は従来どおり直接 ``project up``
を起動して一覧へ戻る (PR1 非回帰)。
"""
name = row["name"]
if str(row.get("status", "")).startswith("running"):
Expand Down
8 changes: 3 additions & 5 deletions lib/devbase/tui/actions_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,11 @@ def run(devbase_root: Path):
"""スナップショット操作カテゴリ。操作選択 → 引数収集 → 実行。

戻り値プロトコル (``flow.menu_loop``。トップループが ``is`` 同一性で判定する):
- **操作を実行した場合**: ``dispatch_group`` の rc (``int``) を返す。
「実行したのでトップへ戻る、rc は呼び出し側が記憶」の意味。
- ``menu.MENU_BACK``: 操作メニューで Esc/← (操作なしでトップへ)。
- ``menu.MENU_BACK``: 操作メニューで Esc/← (トップへ戻る)。操作を実行しても
(出力確認の一時停止後) 操作メニューに留まり、Esc/← で初めてトップへ戻る。
- ``None``: Ctrl-C による全体中止。

引数収集を中止 (``_ARG_CANCEL``) した場合は操作メニューを再表示する。
操作完了後はトップメニューへ復帰する (plan 3.5 状態遷移: Exec → Top)。
操作実行後・引数収集中止 (``_ARG_CANCEL``) のいずれも操作メニューを再表示する。
"""
return flow.menu_loop(_select_operation,
lambda op: _run_operation(devbase_root, op))
38 changes: 22 additions & 16 deletions lib/devbase/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@

ナビ規約: トップ (プロジェクト一覧) は Esc / Ctrl-C で中止 (戻り先なし)。
各カテゴリ・サブメニュー内では Esc / ← で 1 つ前へ戻る (``menu.MENU_BACK``)、
Ctrl-C で全体中止 (``None``)。操作を実行した後は出力を読めるよう Enter を
待ってから一覧を再表示する (``_pause_for_review``)。
Ctrl-C で全体中止 (``None``)。サブメニューで操作を実行した後はトップへ戻らず、
出力を読めるよう Enter を待ってから**同じサブメニューを再表示する** (実行を
繰り返せる。``flow.pause_for_review`` / ``flow.menu_loop``)。Esc / ← で一覧へ
戻ると、直前に選んだプロジェクト行へカーソルを復元する。直接 up する stopped/
unknown 行と status 表示だけは実行後に Enter を待って一覧へ戻る
(``_pause_for_review``)。
"""

from __future__ import annotations
Expand Down Expand Up @@ -69,17 +73,9 @@ def _route(category: str, devbase_root: Path):


def _clear_screen() -> None:
"""端末をクリアしてカーソルを先頭行へ戻す。

トップ一覧 (メニュープロンプト) を画面の先頭行から表示するため、再描画の
直前に呼ぶ。``\\033[2J`` で表示領域を消去、``\\033[3J`` でスクロールバック
も消去、``\\033[H`` でカーソルを左上へ移動する。stdout が非 TTY の場合は
何もしない。
"""
if not sys.stdout.isatty():
return
sys.stdout.write("\033[3J\033[2J\033[H")
sys.stdout.flush()
"""端末をクリアしてカーソルを先頭行へ戻す (トップ一覧の再描画前)。実体は
``menu.clear_screen`` に集約 (サブメニュー再表示と同じ消去処理)。"""
menu.clear_screen()


def _pause_for_review() -> bool:
Expand Down Expand Up @@ -108,12 +104,15 @@ def _pause_for_review() -> bool:
_NO_PROJECTS = object()


def _select_top(rows: list[dict]):
def _select_top(rows: list[dict], default=None):
"""トップ画面: プロジェクト一覧 + 最下部の常設カテゴリメニューから 1 件選ばせる。

カテゴリ (env/plugin/snapshot/status) は一覧の行ではなく、画面最下部に
横並びで常設するメニューバーに置く (←→ で項目間を移動、Enter で決定)。

``default`` には前回選択したプロジェクトの rows index を渡し、サブメニュー
から一覧へ戻ったときに同じ行へカーソルを復元する (範囲外なら無視)。

戻り値: rows の index (``int`` = プロジェクト選択) / カテゴリ key (``str``) /
``_NO_PROJECTS`` (プレースホルダ選択 = 再表示) / ``None`` (Esc・Ctrl-C → 終了)。
プロジェクトとカテゴリは値の型で判別する。件数が多いため文字入力での
Expand All @@ -125,10 +124,13 @@ def _select_top(rows: list[dict]):
else:
# _build_menu_entries は 0 件を想定しない (max() が落ちる) ため迂回する。
choices = [("(プロジェクトがありません)", _NO_PROJECTS)]
# default は choice value (= rows index) と一致したときだけハイライト初期値に
# 使う。行数が変わって範囲外になった場合は先頭ハイライト (None) に戻す。
initial = default if isinstance(default, int) and 0 <= default < len(rows) else None
return menu.select_with_menubar(
"プロジェクトまたは操作を選択 "
"(↑↓ 移動 / 名前で絞り込み / ←→ 下部メニュー / Enter 決定 / Esc・Ctrl-C 終了):",
choices, [(label, key) for key, label in TOP_CATEGORIES])
choices, [(label, key) for key, label in TOP_CATEGORIES], default=initial)


def _top_menu_loop(devbase_root: Path) -> int:
Expand All @@ -141,6 +143,7 @@ def _top_menu_loop(devbase_root: Path) -> int:
判定は必ず ``is`` 同一性で行う (rc=0 を ``None`` / ``MENU_BACK`` と誤マッチさせない)。
"""
last_rc = 0
cursor = None # 直近に選んだプロジェクトの rows index (一覧再表示時のカーソル復元用)
projects_dir = Path(devbase_root) / "projects"
while True:
_clear_screen()
Expand All @@ -150,7 +153,7 @@ def _top_menu_loop(devbase_root: Path) -> int:
# 終了せず案内だけ出して一覧 (カテゴリのみ) を表示する。
logger.info("プロジェクトがありません (%s)。", projects_dir)

sel = _select_top(rows)
sel = _select_top(rows, default=cursor)
if sel is None:
# トップで Esc / Ctrl-C → これまでの実行 rc を返して終了
logger.info("中止しました。")
Expand All @@ -162,6 +165,9 @@ def _top_menu_loop(devbase_root: Path) -> int:
if isinstance(sel, str):
result = _route(sel, devbase_root)
else:
# 選択したプロジェクト位置を記憶し、サブメニューから戻ったとき同じ行へ
# カーソルを復元する。
cursor = sel
result = actions_project.handle_row(devbase_root, rows[sel])

if result is None:
Expand Down
Loading
Loading