@@ -123,6 +123,26 @@ def resolve_editor_cmd(environ=None) -> Optional[list]:
123123 return None
124124
125125
126+ def resolve_editor_display (environ = None ) -> list :
127+ """コマンド提示 (print_command) 用のエディタ argv を解決する。
128+
129+ :func:`resolve_editor_cmd` と異なり ``shutil.which`` による実在チェックは
130+ 行わない。plain SSH では提示コマンドを実行するのは「ユーザの手元 (ローカル)」
131+ であり、コマンドを実行している側 (リモート) に ``code`` が存在する必要は無い
132+ ため、リモートの実在に依存せず必ず非 None を返す。
133+
134+ ``DEVBASE_EDITOR`` があればそれを (シェル風に分割して) 用い、無ければ既定の
135+ ``["code"]`` を返す。
136+ """
137+ env = os .environ if environ is None else environ
138+ explicit = env .get ("DEVBASE_EDITOR" )
139+ if explicit :
140+ parts = shlex .split (explicit )
141+ if parts :
142+ return parts
143+ return ["code" ]
144+
145+
126146def build_attach_uri (container_name : str , workdir : str ) -> str :
127147 """``vscode-remote://attached-container+<hex>/<workdir>`` を組む。
128148
@@ -175,19 +195,28 @@ def _parse_compose_ps_name(stdout: str) -> Optional[str]:
175195
176196
177197def _query_container_name (dev_service_name : str , index : int ,
198+ compose_file = None ,
178199 runner : Optional [Callable ] = None ) -> Optional [str ]:
179200 """実 docker へ問い合わせて dev インスタンスの実コンテナ名を取得する (保険)。
180201
181202 scale 生成 compose ではサービス名が ``{dev}-{index}`` (例 ``dev-1``) になるため
182203 その service token を指定して ``docker compose ps --format json`` を実行する。
204+ ``{dev}-{index}`` サービスは override compose (``.docker-compose.scale.yml``)
205+ 側にしか存在しないため、``compose_file`` が与えられた場合は起動時と同じ
206+ ``-f <compose_file>`` を付与しないと base ``compose.yml`` には無いサービスを
207+ 見に行きほぼ常にフォールバックになる。
183208 取得できなければ None。docker 不在・非0・例外・空はすべて None に握り潰し、
184209 呼び出し側が決定的名へフォールバックできるようにする。
185210 """
186211 run = runner or subprocess .run
187212 service_token = f"{ dev_service_name } -{ index } "
213+ cmd = ["docker" , "compose" ]
214+ if compose_file is not None :
215+ cmd += ["-f" , str (compose_file )]
216+ cmd += ["ps" , "--format" , "json" , service_token ]
188217 try :
189218 proc = run (
190- [ "docker" , "compose" , "ps" , "--format" , "json" , service_token ] ,
219+ cmd ,
191220 capture_output = True , text = True , timeout = 10 ,
192221 )
193222 except Exception : # noqa: BLE001 - docker 不在等は保険なので握り潰す
@@ -201,6 +230,7 @@ def _query_container_name(dev_service_name: str, index: int,
201230
202231
203232def resolve_container_name (dev_service_name : str , project_name : str , index : int = 1 ,
233+ compose_file = None ,
204234 runner : Optional [Callable ] = None ) -> str :
205235 """dev コンテナの実コンテナ名を返す。
206236
@@ -213,7 +243,8 @@ def resolve_container_name(dev_service_name: str, project_name: str, index: int
213243 を全インスタンスへ設定する (volume/compose.py)。COMPOSE_PROJECT_NAME は
214244 project_name と一致するため、docker 問い合わせに失敗しても決定的に組み立てられる。
215245 """
216- queried = _query_container_name (dev_service_name , index , runner = runner )
246+ queried = _query_container_name (dev_service_name , index ,
247+ compose_file = compose_file , runner = runner )
217248 if queried :
218249 return queried
219250 return f"{ project_name } -{ dev_service_name } -{ index } "
@@ -229,24 +260,37 @@ def resolve_workdir(environ=None, project_name: Optional[str] = None) -> str:
229260 return f"/work/{ repo } " if repo else "/work"
230261
231262
232- def decide_action (ctx : EditorContext , editor_cmd : Optional [list ]) -> OpenPlan :
233- """コンテキストとエディタ可用性から起動方針を決める (§2.4 マトリクス)。"""
234- if not editor_cmd :
235- return OpenPlan (
236- "skip" ,
237- "エディタ (code) が見つかりません。VS Code の `code` コマンドを PATH に "
238- "通すか DEVBASE_EDITOR を設定してください" ,
239- )
263+ _NO_EDITOR_REASON = (
264+ "エディタ (code) が見つかりません。VS Code の `code` コマンドを PATH に "
265+ "通すか DEVBASE_EDITOR を設定してください"
266+ )
267+
268+
269+ def decide_action (ctx : EditorContext , editor_available : bool ) -> OpenPlan :
270+ """コンテキストとエディタ可用性から起動方針を決める (§2.4 マトリクス)。
271+
272+ ``editor_available`` はローカルに launch 可能な ``code`` 系コマンドが実在するか
273+ (``resolve_editor_cmd`` が非 None か) を表す。plain SSH の print_command 経路は
274+ 「ユーザの手元 (ローカル) でコマンドを実行する」前提のため、コマンドを実行して
275+ いる側 (リモート) の editor 実在には依存させない (``editor_available`` を見ない)。
276+ """
240277 if not ctx .is_tty :
241278 return OpenPlan ("skip" , "非対話 (非TTY/CI) 環境のため" )
242279 if ctx .in_vscode :
243280 # VS Code 統合ターミナル (ローカル / WSL / Remote-SSH シム)。code が
244- # クライアント側へ委譲するため直接起動でよい。
281+ # クライアント側へ委譲するため直接起動でよい。code シムが無いと委譲
282+ # できないため editor が無ければ skip。
283+ if not editor_available :
284+ return OpenPlan ("skip" , _NO_EDITOR_REASON )
245285 return OpenPlan ("launch" , "VS Code 統合ターミナル経由" )
246286 if ctx .is_ssh :
247287 # plain SSH (VS Code 外)。クライアントへ push する公式手段が無いため
248- # コマンドを提示する degrade。
288+ # 手元で叩くコマンドを提示する degrade。提示先はローカルなのでリモートの
289+ # editor 実在には依存しない。
249290 return OpenPlan ("print_command" , "SSH セッション (VS Code 外) のため" )
291+ # ローカル/WSL 端末。直接 launch するため editor が無ければ skip。
292+ if not editor_available :
293+ return OpenPlan ("skip" , _NO_EDITOR_REASON )
250294 return OpenPlan ("launch" , "ローカル/WSL 端末" )
251295
252296
@@ -259,29 +303,34 @@ def _launch(cmd: list, env: dict) -> None:
259303
260304
261305def open_editor (* , project_name : str , dev_service_name : str , workdir : str ,
262- index : int = 1 , environ = None ,
306+ index : int = 1 , compose_file = None , environ = None ,
263307 isatty : Optional [bool ] = None , system : Optional [str ] = None ,
264308 launcher : Optional [Callable [[list , dict ], None ]] = None ) -> str :
265309 """dev コンテナへ接続した VS Code を開く / コマンド提示 / スキップする。
266310
267311 戻り値は実行された action ('launch' | 'print_command' | 'skip')。例外は
268312 握り潰して warning にし、``up`` 本体を絶対に失敗させない。``isatty`` /
269- ``system`` は :func:`detect_context` への差し替え口 (テスト用)。
313+ ``system`` は :func:`detect_context` への差し替え口 (テスト用)。``compose_file``
314+ は実コンテナ名問い合わせ時に起動と同じ override compose を ``-f`` で渡すため。
270315 """
271316 env = os .environ if environ is None else environ
272317 ctx = detect_context (env , isatty = isatty , system = system )
273- editor = resolve_editor_cmd (env )
274- plan = decide_action (ctx , editor )
318+ editor = resolve_editor_cmd (env ) # launch 用 (which 込み・None あり得る)
319+ display = resolve_editor_display (env ) # print 用 (必ず非 None)
320+ plan = decide_action (ctx , editor_available = bool (editor ))
275321
276- container = resolve_container_name (dev_service_name , project_name , index )
322+ container = resolve_container_name (dev_service_name , project_name , index ,
323+ compose_file = compose_file )
277324 uri = build_attach_uri (container , workdir )
278325
279326 if plan .action == "skip" :
280327 logger .info ("エディタの自動オープンをスキップ: %s" , plan .reason )
281328 return "skip"
282329
283- quoted = " " .join (shlex .quote (c ) for c in editor )
284330 if plan .action == "print_command" :
331+ # 提示コマンドは手元 (ローカル) で実行する前提。ローカルに code が無くても
332+ # 提示できるよう display (which 非依存) を用いる。
333+ quoted = " " .join (shlex .quote (c ) for c in display )
285334 logger .info ("SSH セッションを検出しました (%s)。" , plan .reason )
286335 logger .info (
287336 "手元の VS Code で次を実行するか、VS Code の Remote-SSH 統合ターミナルから "
@@ -290,6 +339,7 @@ def open_editor(*, project_name: str, dev_service_name: str, workdir: str,
290339 logger .info (" %s --folder-uri '%s'" , quoted , uri )
291340 return "print_command"
292341
342+ quoted = " " .join (shlex .quote (c ) for c in editor )
293343 logger .info ("[editor] %s を起動します (%s)" , quoted , plan .reason )
294344 cmd = [* editor , "--folder-uri" , uri ]
295345 try :
0 commit comments