Skip to content

Commit f2082ea

Browse files
takemi-ohamaclaude
andauthored
feat(list): devbase list を対話選択デフォルトに変更 (#39)
* 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> * fix(list): 非 TTY フォールバック判定に stdout.isatty() を追加 `devbase list | cat` や `devbase list > out.txt` のように stdout だけが 非 TTY のケースでも対話選択が起動してしまう問題を修正。stdin / stdout の いずれかが非 TTY なら確実に一覧表示へフォールバックするよう判定を拡張した。 stdout 非 TTY フォールバックのテストも追加。 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 3b38330 commit f2082ea

9 files changed

Lines changed: 125 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: 6 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,11 @@ 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+
# stdin / stdout のいずれかが非 TTY (`devbase list | cat`, `> out.txt` 等) なら
181+
# 対話プロンプトが表示できない / 読めないため、確実に一覧表示へフォールバックする。
182+
if getattr(args, "interactive", True) and sys.stdin.isatty() and sys.stdout.isatty():
178183
return _interactive_select_and_up(rows)
179184

180185
_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: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,56 @@ 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+
249+
def test_cmd_project_list_stdout_non_tty_falls_back_to_table(tmp_path, monkeypatch, capsys):
250+
"""stdin が TTY でも stdout が非 TTY (`devbase list | cat` / `> out.txt`) なら
251+
対話起動せず一覧表示へフォールバックする。"""
252+
from devbase.commands import project as project_mod
253+
from devbase.commands import status as status_mod
254+
from devbase.commands import container as container_mod
255+
256+
_make_plugin_project(tmp_path, "repos/o--r/alpha", "alpha-proj")
257+
_link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj")
258+
monkeypatch.setattr(status_mod, "_container_status_for",
259+
lambda entry: {"name": entry.name, "status": "stopped", "count": 0})
260+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
261+
monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: False)
262+
263+
called = []
264+
monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0)
265+
266+
args = types.SimpleNamespace(interactive=True)
267+
rc = project_mod.cmd_project_list(tmp_path, args)
268+
out = capsys.readouterr().out
269+
270+
assert rc == 0
271+
assert called == [], "stdout 非 TTY では対話起動しない"
272+
assert "alpha-proj" in out
273+
274+
225275
# ---------------------------------------------------------------------------
226276
# cmd_project_list: --interactive
227277
# ---------------------------------------------------------------------------
@@ -237,6 +287,9 @@ def test_cmd_project_list_interactive_selects_and_ups(tmp_path, monkeypatch):
237287
_link_project(tmp_path, "beta-proj", "plugins/beta", "beta-proj")
238288
monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None)
239289

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

@@ -261,6 +314,8 @@ def test_cmd_project_list_interactive_empty_input_aborts(tmp_path, monkeypatch):
261314
_make_plugin_project(tmp_path, "repos/o--r/alpha", "alpha-proj")
262315
_link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj")
263316
monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None)
317+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
318+
monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True)
264319
monkeypatch.setattr("builtins.input", lambda *a, **k: "")
265320

266321
called = []
@@ -285,6 +340,8 @@ def test_cmd_project_list_interactive_non_tty_eof(tmp_path, monkeypatch):
285340
def raise_eof(*a, **k):
286341
raise EOFError
287342

343+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
344+
monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True)
288345
monkeypatch.setattr("builtins.input", raise_eof)
289346
called = []
290347
monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0)
@@ -308,6 +365,8 @@ def test_cmd_project_list_interactive_keyboard_interrupt_aborts(tmp_path, monkey
308365
def raise_interrupt(*a, **k):
309366
raise KeyboardInterrupt
310367

368+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
369+
monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True)
311370
monkeypatch.setattr("builtins.input", raise_interrupt)
312371
called = []
313372
monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0)
@@ -328,6 +387,8 @@ def test_cmd_project_list_interactive_out_of_range_reprompts(tmp_path, monkeypat
328387
_link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj")
329388
monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None)
330389

390+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
391+
monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True)
331392
# "99" (範囲外) → "1" (有効) の順に入力 → 再入力後に up が起動する
332393
inputs = iter(["99", "1"])
333394
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
@@ -351,6 +412,8 @@ def test_cmd_project_list_interactive_non_numeric_reprompts(tmp_path, monkeypatc
351412
_link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj")
352413
monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None)
353414

415+
monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True)
416+
monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True)
354417
# "abc" (数値以外) → "1" (有効)
355418
inputs = iter(["abc", "1"])
356419
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
@@ -369,20 +432,30 @@ def test_cmd_project_list_interactive_non_numeric_reprompts(tmp_path, monkeypatc
369432
# ---------------------------------------------------------------------------
370433

371434
def test_parser_project_list():
435+
# 対話選択はデフォルト ON (フラグ無しで interactive=True)。
372436
parser = cli._create_parser()
373437
args = parser.parse_args(["project", "list"])
374438
assert args.command == "project"
375439
assert args.subcommand == "list"
376-
assert args.interactive is False
440+
assert args.interactive is True
377441

378442

379443
def test_parser_project_list_interactive_flag():
444+
# `-i` / `--interactive` は後方互換で受け付ける (実質 no-op、True のまま)。
380445
parser = cli._create_parser()
381446
for flag in ("--interactive", "-i"):
382447
args = parser.parse_args(["project", "list", flag])
383448
assert args.interactive is True
384449

385450

451+
def test_parser_project_list_no_interactive_flag():
452+
# `--no-interactive` / `--plain` / `-P` で一覧表示のみ (interactive=False)。
453+
parser = cli._create_parser()
454+
for flag in ("--no-interactive", "--plain", "-P"):
455+
args = parser.parse_args(["project", "list", flag])
456+
assert args.interactive is False
457+
458+
386459
def test_parser_top_level_list_synonym():
387460
parser = cli._create_parser()
388461
args = parser.parse_args(["list", "-i"])

0 commit comments

Comments
 (0)