|
| 1 | +# PLAN48: `devbase env init` で `HOST_SSH_USER` を自動設定する |
| 2 | + |
| 3 | +## 関連リンク |
| 4 | + |
| 5 | +- 元 issue: [#48](https://github.com/devbasex/devbase/issues/48) `feat: devbase env init で HOST_SSH_USER を自動設定する` |
| 6 | +- 利用側 (参照のみ・本リポジトリ対象外): ai-plugins `plugins/ndf/skills/playwright-browser-connect/` |
| 7 | + (`scripts/start-host-chrome.sh` がコンテナ→ホスト SSH のために `HOST_SSH_USER` を要求する) |
| 8 | + |
| 9 | +## 概要 |
| 10 | + |
| 11 | +`devbase env init` の対話セットアップに **ホスト接続情報 (SSH)** コレクタを追加し、 |
| 12 | +ホスト (mac/Linux/WSL) のログインユーザー名を `HOST_SSH_USER` として `.env` に自動設定する。 |
| 13 | +併せて SSH 先ホスト名 `HOST_SSH_HOST`(既定 `host.docker.internal`)も収集する。 |
| 14 | + |
| 15 | +これにより、コンテナからホストへ SSH してホスト側 GUI アプリ(例: Chrome をリモート |
| 16 | +デバッグモードで起動)を起動するワークフローを、`HOST_SSH_USER=<名>` の手動指定なしで |
| 17 | +利用できるようにする。 |
| 18 | + |
| 19 | +`devbase env init` は **ホスト上で実行される CLI** であり、ホストのユーザー名を確実に |
| 20 | +取得できる立場にある(`getpass.getuser()`)。 |
| 21 | + |
| 22 | +## 設計判断(確定事項) |
| 23 | + |
| 24 | +| 論点 | 決定 | 理由 | |
| 25 | +|---|---|---| |
| 26 | +| 収集キー | `HOST_SSH_USER` + `HOST_SSH_HOST` 両方 | WSL2/Windows ではホストユーザーと SSH 先が一致しないケースがあり、`HOST_SSH_HOST` の上書き余地を残す(issue 補足) | |
| 27 | +| `devbase env sync` 対応 | あり(欠落キーの補完) | 既存ユーザーの `.env` への後付け backfill。ただし手動上書きは尊重して上書きしない | |
| 28 | +| 進め方 | 単一 PR | 変更 ~4 ファイル・~100 行・依存タスクなし。release ブランチ不要 | |
| 29 | + |
| 30 | +## アーキテクチャ整合 |
| 31 | + |
| 32 | +既存のコレクタ機構に倣う: |
| 33 | + |
| 34 | +- `lib/devbase/env/collector.py` の `CollectorRegistry` は `env/collectors/*.py` を走査し、 |
| 35 | + モジュール直下の **`COLLECTOR` 定数**(大文字)を登録する。issue 例の `collector =` は |
| 36 | + 実装規約と異なるため `COLLECTOR =` で定義する。 |
| 37 | +- 既存 `git.py` / `slack.py` と同じ `Collector(name, display_name, collect_fn)` インターフェース。 |
| 38 | +- 対話入力は `devbase.env.store.safe_input`(EOF 安全。非対話/CI では default を返す)を使う。 |
| 39 | + |
| 40 | +```mermaid |
| 41 | +flowchart LR |
| 42 | + init["devbase env init"] --> reg["CollectorRegistry.discover()"] |
| 43 | + reg --> host["collectors/host.py<br/>COLLECTOR"] |
| 44 | + host --> collect["collect_host_info(env_file)"] |
| 45 | + collect --> envfile[".env<br/>HOST_SSH_USER / HOST_SSH_HOST"] |
| 46 | + sync["devbase env sync"] --> backfill["_sync_host()<br/>欠落キーのみ補完"] |
| 47 | + backfill --> envfile |
| 48 | +``` |
| 49 | + |
| 50 | +## 変更ファイル |
| 51 | + |
| 52 | +| ファイル | 種別 | 内容 | |
| 53 | +|---|---|---| |
| 54 | +| `lib/devbase/env/keys.py` | 変更 | `HOST_SSH_USER` / `HOST_SSH_HOST` 定数を追加 | |
| 55 | +| `lib/devbase/env/collectors/host.py` | 新規 | `host` コレクタ(`collect_host_info` + `COLLECTOR`) | |
| 56 | +| `lib/devbase/commands/env.py` | 変更 | `cmd_env_sync` に `_sync_host()` を追加 | |
| 57 | +| `docs/user/environment-variables.md` | 変更 | `#### host` 節とコレクタ一覧へ追記 | |
| 58 | +| `tests/env/test_collector_host.py` | 新規 | コレクタ + sync の単体テスト | |
| 59 | + |
| 60 | +## 実装詳細 |
| 61 | + |
| 62 | +### 1. `keys.py` |
| 63 | + |
| 64 | +```python |
| 65 | +# --- Host (コンテナ→ホスト SSH 接続) --- |
| 66 | +HOST_SSH_USER = "HOST_SSH_USER" |
| 67 | +HOST_SSH_HOST = "HOST_SSH_HOST" # 任意。default: host.docker.internal |
| 68 | +``` |
| 69 | + |
| 70 | +### 2. `collectors/host.py`(新規) |
| 71 | + |
| 72 | +```python |
| 73 | +"""ホスト接続情報 (SSH) コレクター""" |
| 74 | + |
| 75 | +import getpass |
| 76 | + |
| 77 | +from devbase.log import get_logger |
| 78 | +from devbase.env import keys |
| 79 | +from devbase.env.store import EnvFile, safe_input |
| 80 | +from devbase.env.collector import Collector |
| 81 | + |
| 82 | +logger = get_logger(__name__) |
| 83 | + |
| 84 | +DEFAULT_HOST_SSH_HOST = "host.docker.internal" |
| 85 | + |
| 86 | + |
| 87 | +def _default_host_user() -> str: |
| 88 | + """ホストのログインユーザー名。HOME/USER/LOGNAME 欠落時も例外を出さず "" を返す。""" |
| 89 | + try: |
| 90 | + return getpass.getuser() |
| 91 | + except Exception: |
| 92 | + return "" |
| 93 | + |
| 94 | + |
| 95 | +def collect_host_info(env_file: EnvFile) -> None: |
| 96 | + """ホスト接続情報 (SSH) を対話的に収集する""" |
| 97 | + print("\n=== ホスト接続情報 (SSH) ===") |
| 98 | + |
| 99 | + default_user = env_file.get(keys.HOST_SSH_USER) or _default_host_user() |
| 100 | + user = safe_input(f"{keys.HOST_SSH_USER} [{default_user}]: ", default_user) |
| 101 | + if user: |
| 102 | + env_file.set(keys.HOST_SSH_USER, user) |
| 103 | + else: |
| 104 | + logger.info("%s: 既定値が取得できずスキップ", keys.HOST_SSH_USER) |
| 105 | + |
| 106 | + default_host = env_file.get(keys.HOST_SSH_HOST) or DEFAULT_HOST_SSH_HOST |
| 107 | + host = safe_input(f"{keys.HOST_SSH_HOST} [{default_host}]: ", default_host) |
| 108 | + if host: |
| 109 | + env_file.set(keys.HOST_SSH_HOST, host) |
| 110 | + |
| 111 | + |
| 112 | +COLLECTOR = Collector( |
| 113 | + name="host", |
| 114 | + display_name="ホスト接続情報 (SSH)", |
| 115 | + collect_fn=collect_host_info, |
| 116 | +) |
| 117 | +``` |
| 118 | + |
| 119 | +非対話 (EOF) 時: `safe_input` が default を返すため `HOST_SSH_USER=getpass.getuser()`・ |
| 120 | +`HOST_SSH_HOST=host.docker.internal` が設定される(受け入れ条件「非対話/CI でも既定値で設定」)。 |
| 121 | +`getpass.getuser()` が空を返す環境では `HOST_SSH_USER` は安全にスキップされる。 |
| 122 | + |
| 123 | +### 3. `cmd_env_sync` への `_sync_host()` 追加 |
| 124 | + |
| 125 | +ホスト情報はソースファイルを持たないため hash 比較は使わず、**欠落キーのみ既定値で補完**する |
| 126 | +(既存値=WSL2 等での手動上書きは尊重して上書きしない)。既存ユーザーの `.env` への後付け |
| 127 | +backfill として機能する。 |
| 128 | + |
| 129 | +```python |
| 130 | +def _sync_host(env_file): |
| 131 | + """ホスト接続情報の同期。欠落キーを既定値で補完する。更新件数を返す。""" |
| 132 | + from devbase.env.collectors.host import _default_host_user, DEFAULT_HOST_SSH_HOST |
| 133 | + updated = 0 |
| 134 | + if not env_file.get(keys.HOST_SSH_USER): |
| 135 | + user = _default_host_user() |
| 136 | + if user: |
| 137 | + env_file.set(keys.HOST_SSH_USER, user) |
| 138 | + logger.info("HOST_SSH_USER: %s を設定", user) |
| 139 | + updated += 1 |
| 140 | + if not env_file.get(keys.HOST_SSH_HOST): |
| 141 | + env_file.set(keys.HOST_SSH_HOST, DEFAULT_HOST_SSH_HOST) |
| 142 | + logger.info("HOST_SSH_HOST: %s を設定", DEFAULT_HOST_SSH_HOST) |
| 143 | + updated += 1 |
| 144 | + return updated |
| 145 | +``` |
| 146 | + |
| 147 | +`cmd_env_sync` 内で `updated += _sync_host(env_file)` を呼ぶ。`updated > 0` なら既存ロジック |
| 148 | +どおり `env_file.save()` まで到達する。 |
| 149 | + |
| 150 | +### 4. ドキュメント (`docs/user/environment-variables.md`) |
| 151 | + |
| 152 | +「コレクター一覧」に `host` を追加し、slack 節の後に以下を追記: |
| 153 | + |
| 154 | +```markdown |
| 155 | +#### host -- ホスト接続情報 (SSH) |
| 156 | + |
| 157 | +| キー | 説明 | |
| 158 | +|------|------| |
| 159 | +| `HOST_SSH_USER` | コンテナ→ホスト SSH 時のホストログインユーザー名(既定: `getpass.getuser()`) | |
| 160 | +| `HOST_SSH_HOST` | SSH 先ホスト名(既定: `host.docker.internal`、WSL2/Windows では上書き可) | |
| 161 | + |
| 162 | +ユーザー名のみで秘密情報ではない。SSH 鍵・リモートログイン有効化はホスト側で別途設定する前提。 |
| 163 | +``` |
| 164 | + |
| 165 | +## テスト計画 (`tests/env/test_collector_host.py`) |
| 166 | + |
| 167 | +`input` / `getpass.getuser` を monkeypatch した単体テスト(本コレクタは questionary を |
| 168 | +使わない素の `input`/`safe_input` 経路のため、real TTY は不要): |
| 169 | + |
| 170 | +1. **既定値設定**: `getuser`→"alice"、入力 EOF → `HOST_SSH_USER=alice` / `HOST_SSH_HOST=host.docker.internal` |
| 171 | +2. **上書き**: 入力で "bob" / "192.168.1.10" → その値で設定される |
| 172 | +3. **getuser 例外**: `getuser` が例外 → `HOST_SSH_USER` 未設定・`HOST_SSH_HOST` は既定で設定 |
| 173 | +4. **既存値優先**: env に既存 `HOST_SSH_USER=carol` → default 提示が carol |
| 174 | +5. **`_sync_host` backfill**: 欠落時に補完・既存値は上書きしない・更新件数が正しい |
| 175 | +6. **レジストリ登録**: `CollectorRegistry.discover()` 後に name=="host" が含まれる |
| 176 | + |
| 177 | +既存テスト一式が回帰しないこと(`pytest tests/`)も確認する。 |
| 178 | + |
| 179 | +## 受け入れ条件(issue より) |
| 180 | + |
| 181 | +- [ ] `devbase env init` 実行時に `HOST_SSH_USER` の既定値(ホストのユーザー名)が提示され、確認・変更できる |
| 182 | +- [ ] `.env` に `HOST_SSH_USER` が書き出され、コンテナ内の環境変数として参照できる |
| 183 | +- [ ] 非対話/CI でも既定値(`getpass.getuser()`)で設定される、もしくは安全にスキップされる |
| 184 | +- [ ] `HOST_SSH_HOST`(既定 `host.docker.internal`)を収集し、上書きできる |
| 185 | +- [ ] `devbase env sync` で欠落キーを既定値で補完する(既存値は尊重) |
| 186 | +- [ ] ドキュメント(`docs/user/environment-variables.md`)に `HOST_SSH_USER` / `HOST_SSH_HOST` を追記 |
| 187 | + |
| 188 | +## PR 計画 |
| 189 | + |
| 190 | +| PR | branch | base | 概要 | |
| 191 | +|---|---|---|---| |
| 192 | +| 単一 | `feature/PLAN48-host-ssh-user` | `main` | 上記 4 ファイル変更 + テスト。`/ndf:cross-review` でセルフレビュー後 merge | |
| 193 | + |
| 194 | +release ブランチは作成しない(単一 PR・低結合)。`/ndf:implementation-plan` 確認 → |
| 195 | +実装 → `/ndf:pr` → `/ndf:cross-review` の通常フローで進める。 |
0 commit comments