Skip to content

Commit e2d15e7

Browse files
takemi-ohamaclaude
andcommitted
feat(list): devbase list を対話選択デフォルトに変更
`devbase list` / `devbase project list` を TTY ではデフォルトで対話選択 (番号入力 → project up 起動) にする。一覧表示のみは `--no-interactive` (`--plain` / `-P`)。非 TTY (パイプ/CI 等) では自動的に一覧表示へ フォールバックする。`--interactive` / `-i` は後方互換として維持。 - cli.py: --interactive を default=True 化、--no-interactive 追加 - project.py: sys.stdin.isatty() ゲートで非 TTY 自動フォールバック - bash/zsh 補完、ドキュメント、CHANGELOG を更新 - 非 TTY フォールバックの新規テスト追加 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3b38330 commit e2d15e7

9 files changed

Lines changed: 91 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
### Added
88
- **`devbase project` サブコマンド群を新設**しました (PLAN06)。CWD に依存せずプロジェクト名でコンテナ操作ができます。
99
- `devbase project up/down/ps/logs/scale [name]` で、任意のディレクトリから `$DEVBASE_ROOT/projects/<name>` を対象に操作できます。名前解決はラッパー (`bin/devbase`) が対象ディレクトリへ `cd` してから実行するため、シェル実装の `build` を含む全操作が名前指定で成立します(呼び出し元シェルの作業ディレクトリは変わりません)。存在しない名前はエラーになり候補が提示されます。
10-
- `devbase project list [--interactive|-i]``$DEVBASE_ROOT/projects/` 配下を `NAME` / `PLUGIN` / `STATUS` の一覧表示します。`PLUGIN` 列はシンボリックリンク先から解決するため、PLAN04 の同名衝突 suffix(例 `carmo.takemi`)が付いていても正しいプラグイン名を表示します。`--interactive` では一覧から番号で選択して起動でき、非対話環境では番号入力にフォールバックします
10+
- `devbase project list``$DEVBASE_ROOT/projects/` 配下を `NAME` / `PLUGIN` / `STATUS` の一覧表示します。`PLUGIN` 列はシンボリックリンク先から解決するため、PLAN04 の同名衝突 suffix(例 `carmo.takemi`)が付いていても正しいプラグイン名を表示します。**TTY ではデフォルトで対話選択**になり、一覧から番号で選んだプロジェクトを `project up` で起動します。`--no-interactive``--plain` / `-P`)で一覧表示のみに切り替えられ、パイプ・リダイレクト・CI などの非 TTY 環境では自動的に一覧表示へフォールバックします(`--interactive` / `-i` は後方互換として引き続き受け付けます)
1111
- トップレベルシノニム `devbase up/down/ps/scale [name]` / `devbase build [image]` / `devbase login [index]` / `devbase list` を整備しました(`logs` はシノニムを持たず `devbase project logs` のみ)。
1212
- bash / zsh のシェル補完に `project` グループとプロジェクト名補完(`$DEVBASE_ROOT/projects/` 配下を列挙)を追加しました。
1313
- 利用者向けドキュメント [`docs/user/cli-reference.md`](docs/user/cli-reference.md) / [`docs/user/container-operations.md`](docs/user/container-operations.md)`project` 体系に更新しました。

docs/user/cli-reference.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ graph TD
1818
D --> D1["up / down / ps / logs / scale [name]"]
1919
D --> D3["login [index]"]
2020
D --> D4["build [image]"]
21-
D --> D2["list [--interactive]"]
21+
D --> D2["list [--no-interactive]"]
2222
E --> E1[init / sync / list / set / get / delete / edit / project]
2323
F --> F1[list / install / uninstall / update / info / sync]
2424
F --> F2[repo add / repo remove / repo list / repo refresh]
@@ -292,21 +292,26 @@ devbase build [image]
292292
`$DEVBASE_ROOT/projects/` 配下のプロジェクトを `NAME` / `PLUGIN` / `STATUS` の一覧で
293293
表示します。
294294

295+
TTY(端末)では**デフォルトで対話選択**になり、番号入力で選んだプロジェクトを
296+
`project up` で起動します。パイプ・リダイレクト・CI などの非 TTY 環境では自動的に
297+
一覧表示のみへフォールバックします。
298+
295299
```
296-
devbase project list [--interactive|-i]
297-
devbase list [--interactive|-i]
300+
devbase project list [--no-interactive|--plain|-P]
301+
devbase list [--no-interactive|--plain|-P]
298302
```
299303

300304
| オプション | 説明 |
301305
|-----------|------|
302-
| `--interactive` / `-i` | 一覧から番号で選択し、そのプロジェクトを `project up` で起動 |
306+
| `--no-interactive` / `--plain` / `-P` | 対話選択せず一覧表示のみ |
307+
| `--interactive` / `-i` | (後方互換)対話選択。デフォルトのため通常は不要 |
303308

304309
```bash
305-
# 一覧表示
310+
# 一覧を表示して番号で選択・起動(TTY デフォルト)
306311
devbase list
307312

308-
# 一覧から選んで起動(非対話環境では番号入力にフォールバック
309-
devbase list -i
313+
# 一覧表示のみ(選択しない
314+
devbase list --no-interactive
310315
```
311316

312317
出力例:

docs/user/container-operations.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,17 @@ devbase project logs -f --tail 100
238238
### プロジェクト一覧
239239

240240
```bash
241-
# 全プロジェクトを NAME / PLUGIN / STATUS で一覧表示
241+
# 一覧を表示し、番号で選択して起動(TTY ではこれがデフォルト)
242242
devbase list
243243

244-
# 一覧から選択して起動(非対話環境では番号入力にフォールバック)
245-
devbase list -i
244+
# 選択せず NAME / PLUGIN / STATUS の一覧表示のみ
245+
devbase list --no-interactive # --plain / -P も同義
246246
```
247247

248+
> TTY(端末)では `devbase list` はデフォルトで対話選択になり、番号入力で
249+
> そのプロジェクトを起動します。パイプ・リダイレクト・CI などの非 TTY 環境では
250+
> 自動的に一覧表示のみにフォールバックします。
251+
248252
`devbase project ps` が「対象プロジェクト 1 つのコンテナ状態」を表示するのに対し、
249253
`devbase list` は「全プロジェクトの横断一覧」を表示します。
250254

etc/_devbase

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,11 @@ _devbase() {
150150
;;
151151
list)
152152
_arguments \
153-
'--interactive[Select a project interactively and start it]' \
154-
'-i[Select a project interactively and start it]'
153+
'--no-interactive[Just print the table without interactive selection]' \
154+
'--plain[Just print the table without interactive selection]' \
155+
'-P[Just print the table without interactive selection]' \
156+
'--interactive[(compat) interactive selection, default]' \
157+
'-i[(compat) interactive selection, default]'
155158
;;
156159
project)
157160
case "$words[3]" in
@@ -185,8 +188,11 @@ _devbase() {
185188
;;
186189
list)
187190
_arguments \
188-
'--interactive[Select a project interactively and start it]' \
189-
'-i[Select a project interactively and start it]'
191+
'--no-interactive[Just print the table without interactive selection]' \
192+
'--plain[Just print the table without interactive selection]' \
193+
'-P[Just print the table without interactive selection]' \
194+
'--interactive[(compat) interactive selection, default]' \
195+
'-i[(compat) interactive selection, default]'
190196
;;
191197
*)
192198
_describe -t project-commands 'project command' project_subcommands

etc/devbase-completion.bash

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ _devbase_completions() {
6262
COMPREPLY=($(compgen -W "$(_devbase_project_names)" -- "$cur"))
6363
fi
6464
;;
65-
# list は位置引数を取らず --interactive のみ。`-*` ガードを外し
65+
# list は位置引数を取らず対話制御フラグのみ。`-*` ガードを外し
6666
# 常にフラグ候補を出す (zsh 側 _arguments と挙動を揃える)。
6767
list)
68-
COMPREPLY=($(compgen -W "--interactive -i" -- "$cur"))
68+
COMPREPLY=($(compgen -W "--no-interactive --plain -P --interactive -i" -- "$cur"))
6969
;;
7070
project)
7171
COMPREPLY=($(compgen -W "$project_subcommands" -- "$cur"))
@@ -121,10 +121,10 @@ _devbase_completions() {
121121
COMPREPLY=($(compgen -W "$(_devbase_project_names)" -- "$cur"))
122122
fi
123123
;;
124-
# list は位置引数を取らず --interactive のみ。`-*` ガードを外し
124+
# list は位置引数を取らず対話制御フラグのみ。`-*` ガードを外し
125125
# 常にフラグ候補を出す (zsh 側 _arguments と挙動を揃える)。
126126
list)
127-
COMPREPLY=($(compgen -W "--interactive -i" -- "$cur"))
127+
COMPREPLY=($(compgen -W "--no-interactive --plain -P --interactive -i" -- "$cur"))
128128
;;
129129
esac
130130
fi

lib/devbase/cli.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,18 @@ def _add_project_parser(subparsers):
183183
def _add_list_subparser(sub):
184184
"""`list` サブコマンドを登録する (project list / top-level list 共通)。
185185
186-
NAME / PLUGIN / STATUS の一覧表示。`--interactive` で選択 → `project up` 起動。
186+
NAME / PLUGIN / STATUS の一覧表示。デフォルトで対話選択 → `project up` 起動。
187+
`--no-interactive` (`--plain`) で一覧表示のみ。非 TTY では自動的に一覧のみ。
187188
"""
188189
p = sub.add_parser('list', help='List projects (NAME / PLUGIN / STATUS)')
189-
p.add_argument('--interactive', '-i', action='store_true',
190-
help='Select a project interactively and start it')
190+
# 対話選択をデフォルト ON にする。`-i` / `--interactive` は後方互換のため
191+
# 引き続き受け付ける (既に default=True なので実質 no-op)。
192+
p.add_argument('--interactive', '-i', dest='interactive',
193+
action='store_true', default=True,
194+
help='Select a project interactively and start it (default)')
195+
p.add_argument('--no-interactive', '--plain', '-P', dest='interactive',
196+
action='store_false',
197+
help='Just print the table without interactive selection')
191198

192199

193200
def _add_env_parser(subparsers):

lib/devbase/commands/project.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from __future__ import annotations
1212

1313
import os
14+
import sys
1415
from pathlib import Path
1516

1617
from devbase.log import get_logger
@@ -174,7 +175,9 @@ def cmd_project_list(devbase_root: Path, args) -> int:
174175
logger.info("プロジェクトがありません (%s)。", projects_dir)
175176
return 0
176177

177-
if getattr(args, "interactive", False):
178+
# 対話選択はデフォルト ON。ただし非 TTY (パイプ / CI / リダイレクト) では
179+
# input() が EOFError になり実用にならないため、自動的に一覧表示へフォールバック。
180+
if getattr(args, "interactive", True) and sys.stdin.isatty():
178181
return _interactive_select_and_up(rows)
179182

180183
_print_table(rows)

tests/cli/test_completion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def test_bash_top_level_ps_flag_after_name(fake_root):
135135

136136
def test_bash_project_list_flags(fake_root):
137137
out = _bash_complete("devbase project list '-'", 3, fake_root)
138-
assert set(out) == {"--interactive", "-i"}
138+
assert set(out) == {"--no-interactive", "--plain", "-P", "--interactive", "-i"}
139139

140140

141141
def test_bash_top_level_commands_include_project_and_list(fake_root):

tests/cli/test_project_list.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,30 @@ def test_cmd_project_list_empty(tmp_path, capsys):
222222
assert rc == 0
223223

224224

225+
def test_cmd_project_list_non_tty_falls_back_to_table(tmp_path, monkeypatch, capsys):
226+
"""interactive=True (デフォルト) でも非 TTY では一覧表示にフォールバックする。"""
227+
from devbase.commands import project as project_mod
228+
from devbase.commands import status as status_mod
229+
from devbase.commands import container as container_mod
230+
231+
_make_plugin_project(tmp_path, "repos/o--r/alpha", "alpha-proj")
232+
_link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj")
233+
monkeypatch.setattr(status_mod, "_container_status_for",
234+
lambda entry: {"name": entry.name, "status": "stopped", "count": 0})
235+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: False)
236+
237+
called = []
238+
monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0)
239+
240+
args = types.SimpleNamespace(interactive=True)
241+
rc = project_mod.cmd_project_list(tmp_path, args)
242+
out = capsys.readouterr().out
243+
244+
assert rc == 0
245+
assert called == [], "非 TTY では対話起動しない"
246+
assert "alpha-proj" in out
247+
248+
225249
# ---------------------------------------------------------------------------
226250
# cmd_project_list: --interactive
227251
# ---------------------------------------------------------------------------
@@ -237,6 +261,8 @@ def test_cmd_project_list_interactive_selects_and_ups(tmp_path, monkeypatch):
237261
_link_project(tmp_path, "beta-proj", "plugins/beta", "beta-proj")
238262
monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None)
239263

264+
# 対話選択は TTY 環境でのみ起動するため isatty を True に固定する。
265+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
240266
# 番号 "2" を選択 (sorted: alpha-proj=1, beta-proj=2)
241267
monkeypatch.setattr("builtins.input", lambda *a, **k: "2")
242268

@@ -261,6 +287,7 @@ def test_cmd_project_list_interactive_empty_input_aborts(tmp_path, monkeypatch):
261287
_make_plugin_project(tmp_path, "repos/o--r/alpha", "alpha-proj")
262288
_link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj")
263289
monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None)
290+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
264291
monkeypatch.setattr("builtins.input", lambda *a, **k: "")
265292

266293
called = []
@@ -285,6 +312,7 @@ def test_cmd_project_list_interactive_non_tty_eof(tmp_path, monkeypatch):
285312
def raise_eof(*a, **k):
286313
raise EOFError
287314

315+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
288316
monkeypatch.setattr("builtins.input", raise_eof)
289317
called = []
290318
monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0)
@@ -308,6 +336,7 @@ def test_cmd_project_list_interactive_keyboard_interrupt_aborts(tmp_path, monkey
308336
def raise_interrupt(*a, **k):
309337
raise KeyboardInterrupt
310338

339+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
311340
monkeypatch.setattr("builtins.input", raise_interrupt)
312341
called = []
313342
monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0)
@@ -328,6 +357,7 @@ def test_cmd_project_list_interactive_out_of_range_reprompts(tmp_path, monkeypat
328357
_link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj")
329358
monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None)
330359

360+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
331361
# "99" (範囲外) → "1" (有効) の順に入力 → 再入力後に up が起動する
332362
inputs = iter(["99", "1"])
333363
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
@@ -351,6 +381,7 @@ def test_cmd_project_list_interactive_non_numeric_reprompts(tmp_path, monkeypatc
351381
_link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj")
352382
monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None)
353383

384+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
354385
# "abc" (数値以外) → "1" (有効)
355386
inputs = iter(["abc", "1"])
356387
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
@@ -369,20 +400,30 @@ def test_cmd_project_list_interactive_non_numeric_reprompts(tmp_path, monkeypatc
369400
# ---------------------------------------------------------------------------
370401

371402
def test_parser_project_list():
403+
# 対話選択はデフォルト ON (フラグ無しで interactive=True)。
372404
parser = cli._create_parser()
373405
args = parser.parse_args(["project", "list"])
374406
assert args.command == "project"
375407
assert args.subcommand == "list"
376-
assert args.interactive is False
408+
assert args.interactive is True
377409

378410

379411
def test_parser_project_list_interactive_flag():
412+
# `-i` / `--interactive` は後方互換で受け付ける (実質 no-op、True のまま)。
380413
parser = cli._create_parser()
381414
for flag in ("--interactive", "-i"):
382415
args = parser.parse_args(["project", "list", flag])
383416
assert args.interactive is True
384417

385418

419+
def test_parser_project_list_no_interactive_flag():
420+
# `--no-interactive` / `--plain` / `-P` で一覧表示のみ (interactive=False)。
421+
parser = cli._create_parser()
422+
for flag in ("--no-interactive", "--plain", "-P"):
423+
args = parser.parse_args(["project", "list", flag])
424+
assert args.interactive is False
425+
426+
386427
def test_parser_top_level_list_synonym():
387428
parser = cli._create_parser()
388429
args = parser.parse_args(["list", "-i"])

0 commit comments

Comments
 (0)