diff --git a/issues/PLAN59_editor-open-design.md b/issues/PLAN59_editor-open-design.md new file mode 100644 index 00000000..907eb250 --- /dev/null +++ b/issues/PLAN59_editor-open-design.md @@ -0,0 +1,216 @@ +# PLAN59: `devbase open` の設計 + +要求と受け入れ条件は [PLAN59_editor-open.md](PLAN59_editor-open.md) にある。この文書は「どう作るか」だけを扱う。 + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | 起動中のプロジェクトで、コンテナに触らず dev コンテナへ接続した窓を開く | devbase の利用者(CLI) | +| F2 | 停止中のプロジェクトで、起動してから窓を開く(`up --open` へ委譲) | devbase の利用者(CLI) | +| F3 | 別ディレクトリからプロジェクト名を指定して F1 / F2 を行う | devbase の利用者(CLI) | +| F4 | `devbase list` の起動中のサブメニューの先頭から F1 を選ぶ | devbase の利用者(TUI) | + +## 構成要素 + +| 要素 | 変更 | 責務 | +| --- | --- | --- | +| `cli.py` の `_add_open_subparser(sub, *, with_name)`(新設) | 足す | `open` の引数を登録する。`--open-index N` と `--context NAME` は常に、`[name]` は `with_name=True` のときだけ登録する。`project` とトップレベルは `True`、`container` は `False` で呼ぶ(`_add_profile_subparser` と同じ形) | +| `cli.py` の `SHORTCUTS` / `SUBCMD_MAP` / parser の epilog | 変える | `open` をトップレベルのショートカットと、`project` / `container` のサブコマンドへ加える | +| `bin/devbase` | 変える | `resolve_command` の候補、Python 実装のコマンドの `case`、`_PROJECT_NAME_SUBCOMMANDS`、`_NAME_RESOLVABLE_SHORTCUTS` に `open` を加える | +| `container.py` の `_dispatch_lifecycle` | 変える | `handlers` に `'open'` を足す | +| `container.py` の `cmd_open`(新設) | 足す | 起動中の判定・index の検査・開く処理の呼び出し、または `cmd_up` への委譲 | +| `utils/docker.py` の `running_dev_instances`(`env.py` の `_running_dev_containers` から移す) | 変える | `docker ps` を 1 回呼び、動いている dev インスタンスの `(index, コンテナ名)` を index 順に返す。呼べなければ `None` | +| `env.py` の `_running_dev_containers` | 変える | `running_dev_instances` の結果からコンテナ名だけを返す薄い包みにする。`env token` から見た振る舞いは変えない | +| `container.py` の `_open_editor_at`(`_maybe_open_editor` から切り出し) | 変える | 開く対象(フォルダ / ワークスペース)と接続先を組み、`opener.open_editor` を呼んで action を返す。有効判定と index の解決は持たない | +| `container.py` の `_maybe_open_editor` | 変える | 有効判定と index の解決だけを残し、開く処理は `_open_editor_at` へ渡す。`up` から見た振る舞いは変えない | +| `tui/actions_project.py` の `_RUNNING_OPS` / `_OP_HANDLERS` | 変える | 先頭に `("エディタを開く (open)", "open")`、ハンドラに `dispatch_lifecycle("open", name, open_index=None)`。先頭の理由のコメントを書き替える | +| `etc/devbase-completion.bash` / `etc/_devbase` | 変える | トップレベル・`project`・`container` の候補に `open` を加え、`[name]` の補完を `up` と同じにする | +| `docs/user/cli-reference/02-project.md` / `CHANGELOG.md` | 変える | 利用者向けの説明と変更履歴 | + +`opener.open_editor` / `opener.decide_action` と `cmd_up` は変えない。 + +下の図は呼び出しの関係だけを描く。呼び出しを持たない補完(`etc/`)と文書(`docs/`・`CHANGELOG.md`)は図に含めない。 + +```mermaid +graph TD + W[bin/devbase] --> P[cli.py] + P --> D[_dispatch_lifecycle] + T[TUI actions_project] --> D + D --> O[cmd_open] + O --> R[running_dev_instances] + O -->|起動中| E[_open_editor_at] + O -->|停止中| U[cmd_up] + U --> M[_maybe_open_editor] + M --> E + E --> V[opener.open_editor] +``` + +## 配置 + +### システムの文脈 + +devbase はホストで動き、2 つの外部に触る。docker daemon(`--context` の先を含む)と、ホストの VS Code(`code` CLI)である。`open` の起動中の経路が docker daemon に対して行うのは、読み取りの `docker ps` と、`opener.open_editor` が既存で行う `docker compose ps` だけである。コンテナ・ボリューム・ネットワークは作らない。 + +```mermaid +graph LR + U[利用者の端末] --> C[devbase CLI / TUI(ホスト)] + C -->|docker ps(読み取り)| D[docker daemon] + C -->|code --folder-uri| V[VS Code] + V -->|Dev Containers で接続| D +``` + +### モジュールの置き場所 + +```text +bin/devbase # 入口のシェル。name 解決とコマンドの振り分け +etc/ +├── devbase-completion.bash # bash 補完 +└── _devbase # zsh 補完 +lib/devbase/ +├── cli.py # parser とショートカット +├── commands/container.py # cmd_open / _open_editor_at +├── commands/env.py # _running_dev_containers(包みにする) +├── editor/opener.py # 変えない +├── tui/actions_project.py # 起動中のサブメニュー +└── utils/docker.py # running_dev_instances(移す先) +``` + +## 入出力の契約 + +### コマンド `open` + +| 項目 | 内容 | +| --- | --- | +| 名前 | `devbase open [name]` / `devbase project open [name]` / `devbase container open`。前方一致の `devbase o` も `open` に解決する | +| 入力 | `name`(任意。`project` とトップレベルだけ)、`--open-index N`(任意の整数)、`--context NAME`(任意。空は usage エラー) | +| 出力(成功) | 起動中: `opener.open_editor` が `launch`(エディタを起動)または `print_command`(SSH で手元のコマンドを提示)を返し、終了コード 0。停止中: `cmd_up` の戻り値をそのまま返す | +| 失敗の形 | 下の表 | +| 互換性 | 追加のみ。既存のコマンドと前方一致の解決(`l` → `login`、`project p` → `ps`)は変わらない | + +| 状況 | 終了コード | 出力 | +| --- | --- | --- | +| `--open` / `--no-open` を渡した | 2 | argparse の usage エラー | +| `--open-index` が 0 以下 | 1 | `open index N は 1 以上を指定してください` | +| 起動中で、index が動いているインスタンスに無い | 1 | `dev-N は起動していません。起動中: 1, 2` | +| 起動中の判定の `docker ps` が 0 以外で終わった・呼べなかった | 1 | `docker ps` の失敗の理由(`running_dev_instances` が error で出す)。`up` へは委譲しない | +| 起動中で、`opener.open_editor` が `skip` を返した(非 TTY・`code` が無い) | 1 | `opener` が出す理由(info)。開けなかったことを終了コードで示す | +| `name` が解決できない | 1 | 既存の `_enter_project` の候補提示 | + +### `running_dev_instances(project, dev_service_name, runner=None) -> Optional[list[tuple[int, str]]]` + +`env.py` の `_running_dev_containers` の中身を `utils/docker.py` へ移したもの。判定の方法は変えない。 + +- `docker ps --filter label=com.docker.compose.project= --format '{{.Names}}\t{{.Label "com.docker.compose.service"}}'` を 1 回呼ぶ。`-a` を付けないため、止まっているコンテナは出ない +- サービスのラベルが `{dev}-{1 以上の数字}` の行から `(数字, コンテナ名)` を集め、数字の昇順で返す。同じプロジェクトの DB などは除かれる +- 呼び出しの失敗(例外・0 以外の終了コード)は error ログを出して `None` を返す。動いているものが無い `[]` と区別する +- Compose のファイルを読まないため、`.docker-compose.scale.yml` の有無と構成の補間に左右されない。接続先は環境変数 `DOCKER_CONTEXT` に従う +- `runner` は差し替え口(既定は `subprocess.run`)。`env.py` の呼び出しは既存どおり渡す + +### TUI + +| 項目 | 変更後 | +| --- | --- | +| `_RUNNING_OPS` の先頭 | `("エディタを開く (open)", "open")`。以下は既存の `up` / `down` / `login` … の順 | +| `_OP_HANDLERS["open"]` | `lambda root, name: dispatch_lifecycle("open", name, open_index=None)` | +| `_BACK_TO_TOP_OPS` | 変えない(`open` を含めない) | + +## 処理の流れ + +分岐と合流が主題のため、呼び出しの相手ではなく判定の順に描く。 + +```mermaid +graph TD + A[index を解決
CLI → DEVBASE_OPEN_INDEX → 1] --> B{0 以下か} + B -->|はい| X1[終了コード 1] + B -->|いいえ| C[context を反映し
機密を注入] + C --> Q[docker ps で
動いている index を得る] + Q -->|失敗| X2[終了コード 1
up へは進まない] + Q -->|0 個| U[cmd_up
open_editor=True] + U --> X3[cmd_up の戻り値] + Q -->|1 個以上| K{index が
含まれるか} + K -->|いいえ| X4[終了コード 1
起動中の index を示す] + K -->|はい| E[_open_editor_at] + E -->|skip| X5[終了コード 1] + E -->|launch / print_command| X6[終了コード 0] +``` + +停止中から `cmd_up` へ委譲するときは、`open_index` に**利用者が渡した値**(未指定なら `None`)を渡す。`cmd_up` は既存どおり `DEVBASE_OPEN_INDEX` を読み、範囲外は警告して 1 へ落とす(仕様の前提 3)。 + +## 非機能の実現方式 + +| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | +| --- | --- | --- | --- | +| 性能・拡張性 | 起動中の経路で docker を呼ぶのは、起動中の判定の `docker ps` 1 回と、`opener.open_editor` 内の既存の呼び出しだけ | `cmd_open` は `_run_deploy_pipeline`・`_run_pre_up_checks`・`_auto_snapshot` を呼ばない。起動中の判定は `running_dev_instances` の 1 回に集める | 単体テストで `running_dev_instances` の `runner` と `cmd_up` 系の関数を差し替え、起動中の経路で docker の呼び出しが `docker ps` 1 回だけであることを見る | +| 運用・保守性 | 停止中から `up` へ委譲するときは、その旨を info ログに 1 行出す | `cmd_up` を呼ぶ直前に `dev コンテナが起動していないため up を実行します` を info で出す | 単体テストで caplog を見る | + +## 決定の記録 + +### 決定 1: 起動中の判定は「dev インスタンスが 1 つ以上動いているか」で行い、index の検査と分ける + +issue の案は「開く index のコンテナ名が解決できない → 停止中」だった。この判定では、scale 1 で動いているプロジェクトに `--open-index 2` を渡すと停止中と見なし、`up` で環境を作り直してしまう。窓を開くだけのつもりの操作が、起動済みのコンテナを止めて作り直す操作に化ける。0 個と「その index だけ無い」を分け、前者だけを `up` へ委譲する。 + +`opener.resolve_container_name` は使わない。問い合わせに失敗すると決定的な名前へ落ちる設計で、起動していなくても名前を返すため判定に使えない。 + +### 決定 2: 状態を取得できないときは `up` へ委譲せずに止まる + +起動中の判定の `docker ps` が失敗した(daemon に届かない)ことは、停止中を意味しない。ここで `up` へ進むと、動いている環境を作り直す可能性がある。`up` の側で同じ原因により失敗するとしても、利用者が「なぜ起動が走ったか」を読み違えないよう、`open` の入口で止める。 + +### 決定 3: 開く処理は `_maybe_open_editor` から切り出して共有し、有効判定を持たせない + +開く対象の組み立てだけを `_open_editor_at` に切り出す。組み立てるのは、フォルダかワークスペースか・compose file・接続先の 3 つである。有効判定と index の解決は、呼び出し側がそれぞれ持つ。 + +`open` は明示の操作なので、`DEVBASE_OPEN_EDITOR` と `project.yml` の `open_editor` を見ない(仕様の受け入れ条件 2)。`_maybe_open_editor` へ `open_flag=True` を渡す形は採らない。その経路は範囲外の index を 1 へ落とす(`up` のための既存の振る舞い)。そのため受け入れ条件 5 と両立しない。 + +### 決定 4: 停止中は `cmd_up(open_editor=True)` へ委譲し、自前で開き直さない + +`up` の `[6/6]` がすでに開く処理を持っている。起動の直後に開くための compose file(`_run_deploy_pipeline` が返したもの)も、そこで決まる。`cmd_open` が `up` の後にもう一度開くと、窓が 2 つ出るか、`up` 側の有効判定との二重管理になる。`open_editor=True` を渡すことで、`DEVBASE_OPEN_EDITOR=0` の端末でも窓が開く(受け入れ条件 4)。 + +### 決定 5: `opener.open_editor` が `skip` を返したら終了コード 1 にする + +`up` では窓を開けなくても成功とする(起動が主目的のため)。`open` は窓を開くことだけが目的なので、非 TTY や `code` が無いことで何もしなかった場合は失敗として返す。`print_command`(SSH でコマンドを提示)は利用者が次にすべきことを出しているため成功とする。 + +### 決定 6: TUI の `open` は index を尋ねない + +issue の指定(`open_index=None`)どおり、既定(`DEVBASE_OPEN_INDEX`、無ければ 1)を開く。scale が 2 以上のプロジェクトで別の index を開きたい場合は CLI の `--open-index` を使う。index を尋ねる入力を足すと、Enter 1 回で窓を出すという先頭に置く理由が失われる。 + +### 決定 7: `container open` も足す + +`container` は非推奨だが、`profile`(PLAN58)を含めて `project` と同じサブコマンドの集合を保っている。片方だけにすると、補完と `SUBCMD_MAP` の対応表に例外が 1 つ増える。`container open` は `[name]` を取らない(`container` の他のサブコマンドと同じ)。 + +### 決定 8: 起動中の判定は `env token` の列挙を共有の場所へ移して使う + +`env.py` の `_running_dev_containers` が、動いている dev インスタンスの列挙をすでに持っている。失敗を `None` で返し、0 個の `[]` と区別する契約も、決定 2 が求める形と一致する。`utils/docker.py` へ移して `cmd_open` と `env token` の両方から使う。 + +`docker compose ps` で数える形は採らない。Compose のファイル(`.docker-compose.scale.yml`)が無いと呼べず、構成の補間の失敗も「状態を取得できない」に混ざるためである。`docker ps` とラベルで数える既存の方法は、どちらにも左右されない。 + +## テスト設計 + +| 受け入れ条件 | 何で確かめるか | +| --- | --- | +| 1 | 単体(`tests/commands/test_container_open.py`): 起動中で `opener.open_editor` が 1 回呼ばれ、`_run_deploy_pipeline` / `_run_pre_up_checks` / `_auto_snapshot` / `cmd_up` が呼ばれない。compose の呼び出しは `ps` 1 回 | +| 2 | 単体: `DEVBASE_OPEN_EDITOR=0` と `open_editor: false` のそれぞれで `opener.open_editor` が呼ばれる | +| 3 | 単体: 動いているインスタンスが 0 個で `cmd_up` が `open_editor=True` で 1 回呼ばれ、`opener.open_editor` は呼ばれない。戻り値が `cmd_up` のもの | +| 4 | 単体: 3 と同じ状況で `DEVBASE_OPEN_EDITOR=0` でも `cmd_up` に `open_editor=True` が渡る | +| 5 | 単体: 起動中 `[1]` で index 2 → 1 を返し、`cmd_up` / `opener.open_editor` が呼ばれない。メッセージに `1` を含む | +| 6 | 単体: `project.yml` の scale 1、起動中 `[1, 2]` で index 2 → `opener.open_editor(index=2)` | +| 7 | 単体: index 0 と -1 で 1 を返し、docker を呼ばない | +| 8 | 単体: `DEVBASE_OPEN_INDEX=2`・起動中 `[1, 2]` で `index=2` | +| 9 | CLI(`tests/cli/`): `project open ` とトップレベル `open ` の parse 結果が `name` を持ち、`_dispatch_lifecycle` が `_enter_project` を呼ぶ。`bin/devbase` の 2 つのリストに `open` がある | +| 10 | 単体: `--context X` で `running_dev_instances` を呼ぶ時点の環境変数 `DOCKER_CONTEXT` が `X`、`opener.open_editor` の `docker_context` が `X` | +| 11 | CLI: `open --open` / `open --no-open` が `SystemExit(2)` | +| 12 | TUI(`tests/cli/tui/`): `_RUNNING_OPS[0][1] == "open"` | +| 13 | TUI: `_OP_HANDLERS["open"]` が `dispatch_lifecycle("open", name, open_index=None)` を呼ぶ | +| 14 | TUI: `"open" not in _BACK_TO_TOP_OPS` | +| 15 | 既存の `tests/editor/test_opener.py` と `up` の自動オープンのテストが変更なしで通る | +| 16 | CLI: 既存の前方一致のテストに `o` → `open` を足し、`l` → `login` / `project p` → `ps` が変わらない | +| 17 | 補完(`tests/cli/test_completion.py`): bash / zsh の候補に `open` がある | +| 18 | `uv run pytest` | +| 19 | 単体: `opener.open_editor` が `skip` を返すと 1、`print_command` で 0(決定 5) | +| 20 | 単体: `running_dev_instances` が `None` を返すと 1 を返し、`cmd_up` を呼ばない(決定 2)。`running_dev_instances` 自体は、`{dev}-{数字}` 以外のサービスを除くこと・index 順に並べること・失敗で `None` を返すことを見る。`env token` の既存テスト(`tests/commands/test_env_token.py`)が変更なしで通る | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| 実機での窓の再表示 | VS Code が同じコンテナ・同じフォルダの窓をすでに開いているとき、`code --folder-uri` が既存の窓を前面に出すか新しい窓を開くかは VS Code 側の挙動で、devbase では決めない。リリース後テストで macOS のローカル端末で確かめる | diff --git a/issues/PLAN59_editor-open.md b/issues/PLAN59_editor-open.md new file mode 100644 index 00000000..01f1dced --- /dev/null +++ b/issues/PLAN59_editor-open.md @@ -0,0 +1,183 @@ +# PLAN59: 再起動なしで VS Code だけ開き直す `devbase open` + +対象 issue: devbasex/devbase#197 + +- ワークフローモード: `standard` + - 根拠: 公開インタフェース(CLI のコマンド `open` と `devbase list` の TUI のメニュー項目)を足す。 + `cli.py`・`commands/container.py`・`tui/actions_project.py`・`bin/devbase`・補完にまたがる + +## 目的 + +- `devbase up` で開いた VS Code の窓を手で閉じたあと、コンテナを再作成・再起動せずに同じ窓を出し直せるようにする +- `devbase list` の TUI で起動中のプロジェクトを選んだとき、Enter 1 回目で窓を出せるようにする + +## 前提 + +- 前提 1: コマンド名は `open` とする(issue の第 1 案。`editor` / `code` は採らない)。トップレベル `devbase open`、 + `devbase project open [name]`、`devbase container open` の 3 つの入口を持つ。トップレベルの `o` 始まりの + 既存コマンドは無いため、前方一致の短縮 `devbase o` も一意に `open` へ解決する +- 前提 2: 「起動している」の判定は、**dev サービスのインスタンスが 1 つ以上動いていること**で行う。 + 判定は 2 段に分ける。 + + | 動いているインスタンス | 扱い | + | --- | --- | + | 0 個 | 停止中として `up` へ委譲する | + | 1 個以上 | index が動いているものに含まれるかを検査する | + + issue の案(「コンテナ名が解決できないこと」)のままでは誤る場面がある。scale 1 で動いているプロジェクトへ + `--open-index 2` を渡すと、「停止中」と判定して `up` を走らせてしまう。2 段に分けるのは、issue の + 「index の上限は動いているコンテナの数から決める」「解決できない index はエラーにする」を両立させる解釈である +- 前提 3: 停止中のときは `devbase up --open [--open-index N]` と同じ動きにする。`up` 側の既存の index の扱い + (範囲外は警告して 1 へ落とす)は変えない +- 前提 4: エディタを開く処理(`opener.open_editor`)の既存の判断(非 TTY はスキップ、SSH ではコマンドを + 提示、`code` が無ければスキップ)は変えない。`open` が変えるのは「開くかどうか」の判定 + (`DEVBASE_OPEN_EDITOR` / `project.yml` の `open_editor` を見ない)だけである +- 前提 5: 起動中の判定・開く処理は、`up` と同じく `--context` と `project.local.yml` の docker context に従う + +## 対象範囲 + +含む: + +- コマンド `open`(トップレベル・`project`・`container`)と、その `--open-index N` / `--context NAME` +- `project open [name]` とトップレベル `open [name]` のプロジェクト名の解決(`bin/devbase` の name 解決と + Python 側の chdir) +- `devbase list` の TUI の起動中メニューの先頭に「エディタを開く (open)」を置くこと +- シェル補完(bash / zsh)と利用者向けの CLI リファレンス、CHANGELOG + +含まない: + +- `devbase up` の `--open` / `--no-open` / `--open-index` と `DEVBASE_OPEN_EDITOR` の挙動の変更 +- `opener.open_editor` の起動方針(launch / print_command / skip)の変更 +- 停止中の行(TUI で選ぶと直接 `up` する行)のメニュー追加。停止中の行はこれまでどおり直接 `up` する +- 複数インスタンスをまとめて開くこと(1 回に開くのは 1 つ) +- 新しい型・永続データ・画面の追加(そのためクラス図・ER 図・画面遷移図を作らない) + +## 受け入れ条件 + +コマンド: + +- [ ] 1. 前提: プロジェクトの dev が動いている + 操作: `devbase open` を実行する + 結果: `opener.open_editor` が 1 回だけ呼ばれる。compose up・compose down・ボリューム/ネットワークの作成・ + compose の再生成・`deploy` フック・pre-up チェック・自動スナップショットは、どれも呼ばれない。終了コード 0 +- [ ] 2. 前提: dev が動いており、`DEVBASE_OPEN_EDITOR=0`(または `project.yml` の `open_editor: false`) + 操作: `devbase open` を実行する + 結果: `opener.open_editor` が呼ばれる(自動オープンの無効化は `open` に効かない) +- [ ] 3. 前提: dev が動いていない(動いているインスタンスが 0 個) + 操作: `devbase open` を実行する + 結果: `cmd_up` が `open_editor=True` で 1 回呼ばれ、`open` 自身は `opener.open_editor` を呼ばない。 + 終了コードは `cmd_up` の戻り値 +- [ ] 4. 前提: dev が動いていない、`DEVBASE_OPEN_EDITOR=0` + 操作: `devbase open` を実行する + 結果: 起動後に窓が開く(`cmd_up` へ `open_editor=True` が渡る) +- [ ] 5. 前提: dev-1 だけが動いている + 操作: `devbase open --open-index 2` を実行する + 結果: `cmd_up` も `opener.open_editor` も呼ばれず、動いている index(`1`)を示すエラーを出して終了コード 1 +- [ ] 6. 前提: dev-1 と dev-2 が動いている(`project.yml` の `scale` は 1。`devbase scale` でオンライン変更した状態) + 操作: `devbase open --open-index 2` を実行する + 結果: `opener.open_editor` が `index=2` で呼ばれる(上限は `project.yml` ではなく動いている数から決まる) +- [ ] 7. `--open-index` に 0 以下を渡すと、`cmd_up` も `opener.open_editor` も呼ばれず終了コード 1 +- [ ] 8. `--open-index` を省き env `DEVBASE_OPEN_INDEX=2` があるとき、起動中の経路は `index=2` を使う +- [ ] 9. `devbase project open ` を別ディレクトリから実行すると、`` のプロジェクトを対象にする。 + 解決は `devbase project up ` と同じ。トップレベル `devbase open ` も同じ +- [ ] 10. `devbase open --context NAME` は、2 か所の両方に `NAME` を使う。起動中の判定の `docker ps` と、 + `opener.open_editor` の `docker_context` である +- [ ] 11. `open` は `--open` / `--no-open` を受け付けない(argparse の usage エラー、終了コード 2) +- [ ] 19. 前提: dev が動いており、端末が非 TTY(または `code` が無い) + 操作: `devbase open` を実行する + 結果: `opener.open_editor` が `skip` を返し、終了コード 1。SSH セッションでコマンドを提示した場合(`print_command`)は 0 +- [ ] 20. 前提: 起動中の判定の `docker ps` が 0 以外で終わる(daemon に届かないなど) + 操作: `devbase open` を実行する + 結果: `cmd_up` を呼ばず、状態を取得できない旨を出して終了コード 1 + +TUI: + +- [ ] 12. 起動中の行のサブメニューの先頭の項目が `open` である +- [ ] 13. 起動中の行のサブメニューで `open` を選ぶと、`dispatch_lifecycle("open", , ...)` が呼ばれる +- [ ] 14. `open` の実行後はトップ一覧へ戻らず、同じサブメニューに留まる(`_BACK_TO_TOP_OPS` に含まれない) + +退行しないこと: + +- [ ] 15. `devbase up` の自動オープン(`--open` / `--no-open` / `--open-index` / `DEVBASE_OPEN_EDITOR`)の既存テストが + 変更なしで通る +- [ ] 16. `devbase l` は引き続き `login` に、`devbase project p` は引き続き `ps` に解決する +- [ ] 17. bash / zsh の補完でトップレベル・`project`・`container` の候補に `open` が出る +- [ ] 18. 全体テスト(`uv run pytest`)が通る + +(19・20 は設計で決めた失敗の形を条件へ戻したもの。番号は追記順) + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| 性能・拡張性 | 起動中の経路で docker を呼ぶのは、起動中の判定の `docker ps` 1 回と、`opener.open_editor` 内の既存の呼び出しだけ(`up` のパイプラインを通らない) | +| 運用・保守性 | 停止中から `up` へ委譲するときは、その旨を info ログに 1 行出す(利用者が「なぜ起動が走ったか」を読める) | + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | 足す: `devbase open [name]` / `project open [name]` / `container open`(`--open-index N` / `--context NAME`)。既存は変えない | +| データ | 変わらない | +| 既存の振る舞い | `devbase list` の起動中サブメニューの既定のハイライトが「再起動 (up)」から「エディタを開く (open)」へ変わる。Enter 連打で再起動していた利用者は 1 つ下を選ぶことになる | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テスト | `uv run pytest` | +| 静的解析 | `uv run ruff check lib tests`(導入されていれば) | +| 手動確認 | 起動中のプロジェクトで窓を閉じ、`devbase open` で同じ窓が開き、`docker ps` の dev の `CreatedAt` / `Status` が変わらないことを見る。停止中のプロジェクトで `devbase open` が起動から窓を開くことを見る。`devbase list` で起動中の行を選び、先頭が `open` であることを見る | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | CLI の登録は `lib/devbase/cli.py`、処理は `lib/devbase/commands/container.py`(lifecycle のサブコマンドの置き場)、TUI は `lib/devbase/tui/actions_project.py`。`bin/devbase` の name 解決の 2 つのリストと `cli.py` の対の注記に従う | +| コーディング規約 | 既存のモジュール関数の並びに足す。コメント・ログは日本語、PLAN 番号を引く既存の書き方に合わせる | +| テスト戦略 | 単体: `cmd_open` の分岐(起動中 / 停止中 / index 範囲外)を docker と `cmd_up` を差し替えて見る。CLI: parser と name 解決と補完。TUI: メニューの並びとハンドラ | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 既存テストの実行、`cli.py` と `bin/devbase` と補完の 3 か所の同期 | +| 確認してから行う | `opener.open_editor` の既存の判断の変更、`up` の挙動の変更 | +| 行わない | `up` の自動オープンの置き換え、依頼範囲外のリファクタリング | + +## 未決 + +| 項目 | 誰が決めるか | 期限 | +| --- | --- | --- | +| なし | | | + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| 窓 | dev コンテナへ Dev Containers 拡張で接続した VS Code のウィンドウ | +| 動いているインスタンス | `docker ps`(`-a` なし)に現れ、Compose のプロジェクトのラベルがこのプロジェクトで、サービスのラベルが `{dev}-{index}` のコンテナ | +| index | 開く dev インスタンスの番号(`dev-1` の `1`)。`--open-index N`、未指定なら env `DEVBASE_OPEN_INDEX`、それも無ければ 1 | + +## 依頼(原文) + +> /goal /ndf:development-workflow https://github.com/devbasex/devbase/issues/197 + +issue #197 の本文(抜粋。原文のまま): + +> **一度閉じた VS Code の窓を、コンテナを止めずに開き直す手段が無い。** + +> | やること | 期待 | +> | --- | --- | +> | 起動中のプロジェクトでコマンドを打つ | コンテナに一切触らず、dev コンテナへ接続した窓を開く | +> | 停止中のプロジェクトで打つ | `up` を実行して起動し、そのまま窓を開く(`devbase up` と同じ結果になる) | +> | `devbase list` の TUI | 起動中メニューの**先頭**(再起動 / 停止 / ログインの上)から同じ操作を選べる | + +> | 論点 | 案 | +> | --- | --- | +> | **既定のハイライトが変わる** | (略)**依頼どおり先頭に置くが、`:31` のコメントもあわせて書き替える** | +> | `DEVBASE_OPEN_EDITOR=0` の端末での扱い | **開く。** 明示のコマンド・明示のメニュー選択は意思表示であり、`up` のときの自動オープンの可否とは別に扱う(`opener.is_open_enabled` を見ない) | +> | index の上限 | `project.yml` の `scale` ではなく**動いているコンテナの数**から決める。(略)そこで解決できない index はエラーにする | +> | 停止中のときの動き | **`up` を実行する。** コンテナ名が解決できないことをもって「起動していない」と判定し、`cmd_up` へ委譲して起動から窓を開くところまで通す。 | + +> `devbase up` の `--open` / `--no-open` / `--open-index` は残す。**`open` の新設は `up` の自動オープンを置き換えるものではない**