Skip to content

Commit facbf8a

Browse files
takemi-ohamaclaude
andauthored
feat: Node.js を Active LTS へ更新 + TUI 再ビルドを build --no-cache へ委譲 (#80)
* fix(tui): 再ビルドメニューを build --no-cache へ委譲する TUI の「再ビルド (rebuild --no-cache)」は CLI の `rebuild` を呼んでおり、 rebuild は期限判定 (build --expires=7) でキャッシュを使うためラベルが謳う --no-cache を満たさなかった。no-cache の正規経路である `build --no-cache` へ委譲し、base/project とも無条件で再ビルドされるよう修正する。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(base): Node.js を Active LTS (setup_lts.x) へ更新 EOL を迎えた Node 20 から NodeSource の setup_lts.x へ切り替え、Active LTS (現在 v24 / Krypton) を追従する。バージョンは固定しない。 あわせて Node24/npm11 がデフォルトで作成する /tmp/node-compile-cache や playwright `--with-deps` の sudo apt が残す root 所有ファイルにより、 非 root の `rm -rf /tmp/*` が Permission denied でビルド失敗する問題を クリーンアップの sudo 化で解消する。build --no-cache で v24.18.0 を確認済み。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tui): サブメニューに留まる挙動とカーソル位置復元 操作の使い勝手を 2 点改善する。 - サブメニューで操作を実行した後、トップ一覧へ戻らず同じサブメニューを 再表示して留まる (flow.menu_loop)。出力を読めるよう Enter 待ちを挟み、 Esc/← で初めて一覧へ戻る。env/plugin/snapshot/project/plugin-repo の 全サブメニューが menu_loop 経由のため一括で適用。 - サブメニューから一覧へ戻る際、直前に選んだプロジェクト行へカーソルを 復元する (questionary.select の default に rows index を渡す)。 サブメニュー内で実行した操作の rc は devbase list の終了コードへは伝搬 しなくなる (実行後も留まる設計のため戻り値は MENU_BACK)。stopped/unknown 行の直接 up と status 表示の rc 伝搬は従来どおり維持する。 影響テストを新挙動へ追従 (戻り値 MENU_BACK / 選択スタブ末尾に MENU_BACK / pause 無効化の autouse fixture) し、カーソル復元の新規テストを追加。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tui): サブメニュー再表示の前に画面をクリアする サブメニューから image build 等を実行すると大量の出力が残り、再表示した 操作メニューがその下に埋もれて見づらかった。flow.menu_loop の操作実行後 (Enter 待ちの後) に画面をクリアし、サブメニューを先頭行から描き直す。 画面クリア処理を menu.clear_screen に集約し、トップ一覧の app._clear_screen からも委譲する (サブメニュー再表示と同じ消去処理を共有)。引数収集中止 (_ARG_CANCEL) では出力が無いためクリアしない。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tui): up/down は実行後トップ一覧へ戻る (他はサブメニューに留まる) 再起動 (up) / 停止 (down) はコンテナの起動状態が大きく変わるため、実行後は サブメニューに留まらずトップ一覧へ戻り最新状態を再表示する。それ以外の login/ps/logs/scale/build/rebuild は連続操作できるようサブメニューに留まる。 flow.menu_loop に back_after 述語を追加し、True を返す操作は実行後 (出力確認の 一時停止のあと) MENU_BACK を返してトップへ戻す。actions_project は _BACK_TO_TOP_OPS={up, down} を渡す。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3c6d27e commit facbf8a

13 files changed

Lines changed: 405 additions & 142 deletions

‎containers/base/Dockerfile‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
3636
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg; \
3737
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
3838
> /etc/apt/sources.list.d/github-cli.list; \
39-
# NodeSource リポジトリ
40-
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -; \
39+
# NodeSource リポジトリ (Active LTS を追従。バージョン固定しない)
40+
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -; \
4141
# ブラウザ: amd64はChrome+Chromium、arm64はChromium
4242
arch="$(dpkg --print-architecture)"; \
4343
BROWSER_PKG=""; \
@@ -144,8 +144,10 @@ RUN set -eux; \
144144
fi; \
145145
# Playwright browsers
146146
npx playwright install --with-deps chromium; \
147-
# クリーンアップ
148-
rm -rf /tmp/* \
147+
# クリーンアップ。--with-deps の sudo apt や Node24/npm11 の
148+
# /tmp/node-compile-cache が root 所有ファイルを残すため sudo で削除する
149+
# (非 root の rm では Permission denied になりビルドが失敗する)。
150+
sudo rm -rf /tmp/* \
149151
~/.cache \
150152
~/.npm \
151153
~/.config/yarn \

‎lib/devbase/tui/actions_env.py‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,11 @@ def run(devbase_root: Path):
179179
"""環境変数カテゴリのエントリ。操作選択 → 引数収集 → cmd_env へ委譲。
180180
181181
戻り値プロトコル (``flow.menu_loop``。トップループが ``is`` 同一性で判定する):
182-
- **操作を実行した場合**: dispatch の rc (``int``) を返す。「実行したのでトップへ
183-
戻る、rc は呼び出し側が記憶」の意味で、失敗が ``devbase list`` の終了コードへ
184-
伝搬する。
185-
- ``menu.MENU_BACK``: サブメニューで Esc/← (操作なしでトップへ)。
182+
- ``menu.MENU_BACK``: サブメニューで Esc/← (トップへ戻る)。操作を実行しても
183+
(出力確認の一時停止後) サブメニューに留まり、Esc/← で初めてトップへ戻る。
186184
- ``None``: Ctrl-C による全体中止。
187185
188-
引数収集を中止 (``_ARG_CANCEL``) した場合はサブメニューを再表示する。
186+
操作実行後・引数収集中止 (``_ARG_CANCEL``) のいずれもサブメニューを再表示する。
189187
"""
190188
return flow.menu_loop(_select_action,
191189
lambda op: _run_operation(devbase_root, op))

‎lib/devbase/tui/actions_plugin.py‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,9 @@ def _run_repo_operation(devbase_root: Path, op: str):
269269
def _repo_menu(devbase_root: Path):
270270
"""plugin repo のサブ階層メニューを回す。
271271
272-
戻り値 (``flow.menu_loop`` のプロトコル): dispatch の rc (``int``) /
273-
``menu.MENU_BACK`` (Esc・← で plugin メニューへ戻る) / ``None`` (Ctrl-C 全体中止)。
274-
引数収集を中止 (``_ARG_CANCEL``) した場合はサブ階層メニューを再表示する。
272+
戻り値 (``flow.menu_loop`` のプロトコル): ``menu.MENU_BACK`` (Esc・← で plugin
273+
メニューへ戻る) / ``None`` (Ctrl-C 全体中止)。操作実行後は (出力確認の一時停止
274+
後) サブ階層メニューに留まり、引数収集を中止 (``_ARG_CANCEL``) した場合も再表示する。
275275
"""
276276
return flow.menu_loop(_select_repo_operation,
277277
lambda op: _run_repo_operation(devbase_root, op))
@@ -281,19 +281,20 @@ def run(devbase_root: Path):
281281
"""プラグイン操作カテゴリ。操作選択 → 引数収集 → ``cmd_plugin`` へ委譲。
282282
283283
戻り値プロトコル (トップループが ``is`` 同一性で判定する。actions_project と同じ):
284-
- **操作を実行した場合**: dispatch の rc (``int``) を返す。失敗 (非0) は
285-
``devbase list`` の終了コードへ伝搬する。
286-
- ``menu.MENU_BACK``: 操作なしでトップメニューへ戻る (Esc/←)。
284+
- ``menu.MENU_BACK``: plugin メニューで Esc/← (トップメニューへ戻る)。操作を
285+
実行しても (出力確認の一時停止後) plugin メニューに留まり、Esc/← で初めて
286+
トップへ戻る。
287287
- ``None``: Ctrl-C による全体中止。
288288
289-
repo はサブ階層メニュー (``_repo_menu``) へ分岐し、Esc/← で plugin メニューへ
290-
戻れる (``MENU_BACK`` を ``_ARG_CANCEL`` 相当に読み替えて再表示する)。
291-
操作完了後はトップメニューへ復帰する (plan 3.5 状態遷移: Exec → Top)。
289+
repo はサブ階層メニュー (``_repo_menu``) へ分岐する。repo 側も実行後はそこに
290+
留まり、Esc/← (``MENU_BACK``) で plugin メニューへ戻る (``_ARG_CANCEL`` 相当に
291+
読み替えて再表示する)。
292292
"""
293293
def _run(op):
294294
if op == "repo":
295295
rc = _repo_menu(devbase_root)
296-
# repo 階層から Esc で戻ったら plugin メニューを再表示する。
296+
# repo 階層は自身で操作を完結する。Esc/← (MENU_BACK) で戻ったら plugin
297+
# メニューを再表示し、Ctrl-C (None) はそのまま全体中止へ伝搬する。
297298
return _ARG_CANCEL if rc is menu.MENU_BACK else rc
298299
return _run_operation(devbase_root, op)
299300

‎lib/devbase/tui/actions_project.py‎

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
("再ビルド (rebuild --no-cache)", "rebuild"),
4141
]
4242

43+
# 実行後にサブメニューへ留まらずトップ一覧へ戻る操作。up/down はコンテナの
44+
# 起動状態が大きく変わるため、最新状態の一覧を見せる方が自然 (それ以外の
45+
# login/ps/logs/scale/build/rebuild は連続操作できるようサブメニューに留まる)。
46+
_BACK_TO_TOP_OPS = frozenset({"up", "down"})
47+
4348
# 中止系番兵は flow と同一オブジェクトを再公開する (呼び出し側・テストの契約)。
4449
_ARG_CANCEL = flow.ARG_CANCEL
4550
_ABORT = flow.ABORT
@@ -119,12 +124,16 @@ def _op_build(devbase_root: Path, name: str):
119124

120125

121126
_OP_HANDLERS = {
122-
# up/down/rebuild/ps は引数なしで即実行。up/rebuild は scale 属性を参照する
123-
# (常に None。他コマンドは無視する)。down はデータを失わない (volume 保持・
124-
# up で復旧可能) ためメニュー選択を意思表示とみなし、確認プロンプトを出さない。
127+
# up/down/ps は引数なしで即実行。up は scale 属性を参照する (常に None。他
128+
# コマンドは無視する)。down はデータを失わない (volume 保持・up で復旧可能)
129+
# ためメニュー選択を意思表示とみなし、確認プロンプトを出さない。
125130
# ps の --all は CLI 既定 (False) に揃える。
126131
"up": lambda root, name: dispatch_lifecycle("up", name, scale=None),
127-
"rebuild": lambda root, name: dispatch_lifecycle("rebuild", name, scale=None),
132+
# 「再ビルド (rebuild --no-cache)」はラベル通り base/project とも無条件 no-cache
133+
# で再ビルドする。CLI の `rebuild` は期限判定 (= build --expires=7) でキャッシュ
134+
# を使い、新しいイメージはスキップするため --no-cache を満たさない。no-cache の
135+
# 正規経路は `build --no-cache` なので build に no_cache=True を渡して委譲する。
136+
"rebuild": lambda root, name: dispatch_lifecycle("build", name, no_cache=True),
128137
"down": lambda root, name: dispatch_lifecycle("down", name),
129138
"login": _op_login,
130139
"ps": lambda root, name: dispatch_lifecycle("ps", name, all=False),
@@ -152,27 +161,34 @@ def _run_operation(devbase_root: Path, name: str, op: str):
152161
def _operation_menu(devbase_root: Path, name: str):
153162
"""running 行の操作サブメニューを回す。
154163
155-
戻り値 (``flow.menu_loop`` のプロトコル): dispatch の rc (``int``) /
156-
``menu.MENU_BACK`` (Esc・← で一覧へ戻る) / ``None`` (Ctrl-C 全体中止)。
157-
引数収集を中止 (``_ARG_CANCEL``) した場合はサブメニューを再表示する。
164+
戻り値 (``flow.menu_loop`` のプロトコル): ``menu.MENU_BACK`` (Esc・← で一覧へ
165+
戻る) / ``None`` (Ctrl-C 全体中止)。多くの操作は実行後もトップ一覧へ戻らず、
166+
出力を読めるよう一時停止してから**同じサブメニューを再表示する** (留まる)。
167+
ただし up/down (``_BACK_TO_TOP_OPS``) は状態が大きく変わるため実行後はトップ
168+
一覧へ戻り最新状態を再表示する。引数収集を中止 (``_ARG_CANCEL``) した場合も
169+
同じサブメニューを再表示する。
158170
"""
159171
return flow.menu_loop(
160172
lambda: _select_action(name),
161-
lambda op: _run_operation(devbase_root, name, op))
173+
lambda op: _run_operation(devbase_root, name, op),
174+
back_after=lambda op: op in _BACK_TO_TOP_OPS)
162175

163176

164177
def handle_row(devbase_root: Path, row: dict):
165178
"""一覧で選択された 1 プロジェクト行を処理する (トップ画面から呼ばれる)。
166179
167180
戻り値プロトコル (トップループが ``is`` 同一性で判定する):
168-
- **操作を実行した場合**: ``dispatch_lifecycle`` の rc (``int``) を返す。
169-
「実行したので一覧へ戻る、rc は呼び出し側が記憶」の意味。これにより
170-
project 操作の失敗が ``devbase list`` の終了コードへ伝搬する。
171-
- ``menu.MENU_BACK``: 操作サブメニューで Esc/← (操作なしで一覧へ)。
181+
- ``menu.MENU_BACK``: 操作サブメニューで Esc/← (一覧へ戻る)。running 行は操作を
182+
実行しても (出力確認の一時停止後) サブメニューに留まり、Esc/← で初めて一覧へ
183+
戻るため、この値を返す。
184+
- **直接 up を実行した場合** (stopped / unknown 行): ``dispatch_lifecycle`` の
185+
rc (``int``) を返す。トップループが rc を記憶し ``devbase list`` の終了コードへ
186+
伝搬する。
172187
- ``None``: サブメニューで Ctrl-C による全体中止。
173188
174-
選択行が running 中なら ``_operation_menu`` で全操作を選ばせ、それ以外
175-
(stopped / unknown) は従来どおり直接 ``project up`` を起動する (PR1 非回帰)。
189+
選択行が running 中なら ``_operation_menu`` で全操作を選ばせ (実行後もサブ
190+
メニューに留まる)、それ以外 (stopped / unknown) は従来どおり直接 ``project up``
191+
を起動して一覧へ戻る (PR1 非回帰)。
176192
"""
177193
name = row["name"]
178194
if str(row.get("status", "")).startswith("running"):

‎lib/devbase/tui/actions_snapshot.py‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,11 @@ def run(devbase_root: Path):
178178
"""スナップショット操作カテゴリ。操作選択 → 引数収集 → 実行。
179179
180180
戻り値プロトコル (``flow.menu_loop``。トップループが ``is`` 同一性で判定する):
181-
- **操作を実行した場合**: ``dispatch_group`` の rc (``int``) を返す。
182-
「実行したのでトップへ戻る、rc は呼び出し側が記憶」の意味。
183-
- ``menu.MENU_BACK``: 操作メニューで Esc/← (操作なしでトップへ)。
181+
- ``menu.MENU_BACK``: 操作メニューで Esc/← (トップへ戻る)。操作を実行しても
182+
(出力確認の一時停止後) 操作メニューに留まり、Esc/← で初めてトップへ戻る。
184183
- ``None``: Ctrl-C による全体中止。
185184
186-
引数収集を中止 (``_ARG_CANCEL``) した場合は操作メニューを再表示する。
187-
操作完了後はトップメニューへ復帰する (plan 3.5 状態遷移: Exec → Top)。
185+
操作実行後・引数収集中止 (``_ARG_CANCEL``) のいずれも操作メニューを再表示する。
188186
"""
189187
return flow.menu_loop(_select_operation,
190188
lambda op: _run_operation(devbase_root, op))

‎lib/devbase/tui/app.py‎

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

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

7074

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

8480

8581
def _pause_for_review() -> bool:
@@ -108,12 +104,15 @@ def _pause_for_review() -> bool:
108104
_NO_PROJECTS = object()
109105

110106

111-
def _select_top(rows: list[dict]):
107+
def _select_top(rows: list[dict], default=None):
112108
"""トップ画面: プロジェクト一覧 + 最下部の常設カテゴリメニューから 1 件選ばせる。
113109
114110
カテゴリ (env/plugin/snapshot/status) は一覧の行ではなく、画面最下部に
115111
横並びで常設するメニューバーに置く (←→ で項目間を移動、Enter で決定)。
116112
113+
``default`` には前回選択したプロジェクトの rows index を渡し、サブメニュー
114+
から一覧へ戻ったときに同じ行へカーソルを復元する (範囲外なら無視)。
115+
117116
戻り値: rows の index (``int`` = プロジェクト選択) / カテゴリ key (``str``) /
118117
``_NO_PROJECTS`` (プレースホルダ選択 = 再表示) / ``None`` (Esc・Ctrl-C → 終了)。
119118
プロジェクトとカテゴリは値の型で判別する。件数が多いため文字入力での
@@ -125,10 +124,13 @@ def _select_top(rows: list[dict]):
125124
else:
126125
# _build_menu_entries は 0 件を想定しない (max() が落ちる) ため迂回する。
127126
choices = [("(プロジェクトがありません)", _NO_PROJECTS)]
127+
# default は choice value (= rows index) と一致したときだけハイライト初期値に
128+
# 使う。行数が変わって範囲外になった場合は先頭ハイライト (None) に戻す。
129+
initial = default if isinstance(default, int) and 0 <= default < len(rows) else None
128130
return menu.select_with_menubar(
129131
"プロジェクトまたは操作を選択 "
130132
"(↑↓ 移動 / 名前で絞り込み / ←→ 下部メニュー / Enter 決定 / Esc・Ctrl-C 終了):",
131-
choices, [(label, key) for key, label in TOP_CATEGORIES])
133+
choices, [(label, key) for key, label in TOP_CATEGORIES], default=initial)
132134

133135

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

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

167173
if result is None:

0 commit comments

Comments
 (0)