44import argparse
55import os
66import sys
7+ from importlib import import_module
78from pathlib import Path
89
910from devbase .errors import DevbaseError
@@ -86,6 +87,14 @@ def _require_devbase_root() -> Path:
8687 return Path (root )
8788
8889
90+ def _add_name_arg (parser ):
91+ """省略可能な `[name]` positional (プロジェクト名) を登録する。
92+
93+ `project <sub> [name]` とトップレベルショートカットで同一定義を共有する。
94+ """
95+ parser .add_argument ('name' , nargs = '?' , default = None , help = 'Project name' )
96+
97+
8998def _add_login_subparser (sub ):
9099 """`login` サブコマンドを登録する (project / container 共通)。
91100
@@ -156,27 +165,24 @@ def _add_project_parser(subparsers):
156165 pj_parser = subparsers .add_parser ('project' , help = 'Manage projects (CWD-independent)' )
157166 pj_sub = pj_parser .add_subparsers (dest = 'subcommand' )
158167
159- pj_up = pj_sub .add_parser ('up' , help = 'Start containers' )
160- pj_up .add_argument ('name' , nargs = '?' , default = None , help = 'Project name' )
161-
162- pj_down = pj_sub .add_parser ('down' , help = 'Stop and remove containers' )
163- pj_down .add_argument ('name' , nargs = '?' , default = None , help = 'Project name' )
168+ _add_name_arg (pj_sub .add_parser ('up' , help = 'Start containers' ))
169+ _add_name_arg (pj_sub .add_parser ('down' , help = 'Stop and remove containers' ))
164170
165171 _add_login_subparser (pj_sub )
166172
167173 pj_ps = pj_sub .add_parser ('ps' , help = 'Show container status' )
168- pj_ps . add_argument ( 'name' , nargs = '?' , default = None , help = 'Project name' )
174+ _add_name_arg ( pj_ps )
169175 pj_ps .add_argument ('--all' , '-a' , action = 'store_true' , help = 'Show all containers' )
170176
171177 pj_logs = pj_sub .add_parser ('logs' , help = 'Show container logs' )
172- pj_logs . add_argument ( 'name' , nargs = '?' , default = None , help = 'Project name' )
178+ _add_name_arg ( pj_logs )
173179 pj_logs .add_argument ('--follow' , '-f' , action = 'store_true' , help = 'Follow log output' )
174180 pj_logs .add_argument ('--tail' , type = int , default = None , help = 'Number of lines' )
175181
176182 # NOTE: `[name]` optional + `new_scale` 必須 int の順。値が 1 個なら new_scale に、
177183 # 2 個なら (name, new_scale) に割り当てられ曖昧にならない (tests/cli 参照)。
178184 pj_scale = pj_sub .add_parser ('scale' , help = 'Scale containers online' )
179- pj_scale . add_argument ( 'name' , nargs = '?' , default = None , help = 'Project name' )
185+ _add_name_arg ( pj_scale )
180186 pj_scale .add_argument ('new_scale' , type = int , help = 'New number of containers' )
181187
182188 _add_build_subparser (pj_sub )
@@ -185,9 +191,8 @@ def _add_project_parser(subparsers):
185191 # 省略可能な `[name]` を取り、name 指定時は _dispatch_lifecycle が chdir してから
186192 # 実行する。wrapper の _PROJECT_NAME_SUBCOMMANDS / _NAME_RESOLVABLE_SHORTCUTS にも
187193 # 追加すること。
188- pj_rebuild = pj_sub .add_parser (
189- 'rebuild' , help = 'Rebuild images without cache (docker compose build --no-cache)' )
190- pj_rebuild .add_argument ('name' , nargs = '?' , default = None , help = 'Project name' )
194+ _add_name_arg (pj_sub .add_parser (
195+ 'rebuild' , help = 'Rebuild images without cache (docker compose build --no-cache)' ))
191196
192197 # `list` は lifecycle ではなく一覧表示 (commands/project.py)。name positional は
193198 # 取らない (wrapper の _PROJECT_NAME_SUBCOMMANDS にも含めない)。
@@ -243,6 +248,12 @@ def _add_env_parser(subparsers):
243248 env_sub .add_parser ('edit' , help = 'Open .env in editor' )
244249 env_sub .add_parser ('project' , help = 'Setup project-specific variables' )
245250
251+ _add_env_export_parser (env_sub )
252+ _add_env_import_parser (env_sub )
253+
254+
255+ def _add_env_export_parser (env_sub ):
256+ """`env export` サブコマンドを登録する。"""
246257 env_export = env_sub .add_parser (
247258 'export' ,
248259 help = 'Export .env files as an encrypted bundle (age)' ,
@@ -278,6 +289,9 @@ def _add_env_parser(subparsers):
278289 '(per-object SSE is always applied regardless of this flag). '
279290 'Has no effect for non-s3:// destinations.' )
280291
292+
293+ def _add_env_import_parser (env_sub ):
294+ """`env import` サブコマンドを登録する。"""
281295 env_import = env_sub .add_parser (
282296 'import' ,
283297 help = 'Import .env files from a bundle (age-encrypted or plaintext tar.gz)' ,
@@ -421,29 +435,24 @@ def _add_shortcuts(subparsers):
421435 注記参照): bin/devbase が build を shell 実装 (cmd_build) に委譲するため、
422436 Python 側でトップレベル build を広告すると実経路と乖離する。
423437 """
424- login_sc = subparsers .add_parser ('login' , help = 'Login to container' )
425- login_sc .add_argument ('index' , nargs = '?' , default = '1' , help = 'Container index' )
438+ _add_login_subparser (subparsers )
426439
427440 ps_sc = subparsers .add_parser ('ps' , help = 'Show container status' )
428- ps_sc . add_argument ( 'name' , nargs = '?' , default = None , help = 'Project name' )
441+ _add_name_arg ( ps_sc )
429442 ps_sc .add_argument ('--all' , '-a' , action = 'store_true' , help = 'Show all containers' )
430443
431- up_sc = subparsers .add_parser ('up' , help = 'Start containers' )
432- up_sc .add_argument ('name' , nargs = '?' , default = None , help = 'Project name' )
433-
434- down_sc = subparsers .add_parser ('down' , help = 'Stop and remove containers' )
435- down_sc .add_argument ('name' , nargs = '?' , default = None , help = 'Project name' )
444+ _add_name_arg (subparsers .add_parser ('up' , help = 'Start containers' ))
445+ _add_name_arg (subparsers .add_parser ('down' , help = 'Stop and remove containers' ))
436446
437447 # `[name]` optional + `new_scale` 必須 int の順 (project scale と同じ規則)。
438448 scale_sc = subparsers .add_parser ('scale' , help = 'Scale containers online' )
439- scale_sc . add_argument ( 'name' , nargs = '?' , default = None , help = 'Project name' )
449+ _add_name_arg ( scale_sc )
440450 scale_sc .add_argument ('new_scale' , type = int , help = 'New number of containers' )
441451
442452 # `rebuild` は project rebuild のトップレベルシノニム (Python 実装のため build と
443453 # 異なりショートカット可)。up/down と同じく `[name]` を受け付ける。
444- rebuild_sc = subparsers .add_parser (
445- 'rebuild' , help = 'Rebuild images without cache (docker compose build --no-cache)' )
446- rebuild_sc .add_argument ('name' , nargs = '?' , default = None , help = 'Project name' )
454+ _add_name_arg (subparsers .add_parser (
455+ 'rebuild' , help = 'Rebuild images without cache (docker compose build --no-cache)' ))
447456
448457 # `list` は `project list` のトップレベルシノニム。lifecycle ではなく一覧表示
449458 # のため SHORTCUTS (project lifecycle へ写像) ではなく _dispatch で個別に
@@ -569,6 +578,17 @@ def main():
569578 return 1
570579
571580
581+ # DEVBASE_ROOT 必須コマンドの定義: cmd -> (module, function, args を渡すか)。
582+ # 起動コストを抑えるため import は dispatch 時に遅延させる (従来の関数内 import と同等)。
583+ _ROOT_COMMANDS = {
584+ 'init' : ('devbase.commands.init' , 'cmd_init' , False ),
585+ 'status' : ('devbase.commands.status' , 'cmd_status' , False ),
586+ 'env' : ('devbase.commands.env' , 'cmd_env' , True ),
587+ 'plugin' : ('devbase.commands.plugin' , 'cmd_plugin' , True ),
588+ 'snapshot' : ('devbase.commands.snapshot' , 'cmd_snapshot' , True ),
589+ }
590+
591+
572592def _dispatch (cmd , args ):
573593 """Dispatch command to handler."""
574594 # Resolve group aliases
@@ -604,30 +624,14 @@ def _dispatch(cmd, args):
604624 return cmd_container (args )
605625
606626 # --- Commands requiring DEVBASE_ROOT ---
627+ spec = _ROOT_COMMANDS .get (cmd )
628+ if spec is None :
629+ logger .error ("Unknown command: '%s'" , cmd )
630+ return 1
631+ module_name , func_name , takes_args = spec
632+ func = getattr (import_module (module_name ), func_name )
607633 devbase_root = _require_devbase_root ()
608-
609- if cmd == 'init' :
610- from devbase .commands .init import cmd_init
611- return cmd_init (devbase_root )
612-
613- if cmd == 'status' :
614- from devbase .commands .status import cmd_status
615- return cmd_status (devbase_root )
616-
617- if cmd == 'env' :
618- from devbase .commands .env import cmd_env
619- return cmd_env (devbase_root , args )
620-
621- if cmd == 'plugin' :
622- from devbase .commands .plugin import cmd_plugin
623- return cmd_plugin (devbase_root , args )
624-
625- if cmd == 'snapshot' :
626- from devbase .commands .snapshot import cmd_snapshot
627- return cmd_snapshot (devbase_root , args )
628-
629- logger .error ("Unknown command: '%s'" , cmd )
630- return 1
634+ return func (devbase_root , args ) if takes_args else func (devbase_root )
631635
632636
633637if __name__ == '__main__' :
0 commit comments