Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 0
feat: PLAN06-2 wrapper cd によるプロジェクト名解決 + トップレベルシノニム#35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
e3bba69bab05af30d5f5369087deFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -180,21 +180,109 @@ resolve_command() { | ||
| fi | ||
| } | ||
| # =================================================================== | ||
| # Project name resolution (PLAN06 Task 2) | ||
| # =================================================================== | ||
| # `devbase project <sub> <name>` および同義のトップレベルシノニム | ||
| # `devbase <sub> <name>` の <name> が $DEVBASE_ROOT/projects/<name> に実在する | ||
| # 場合、そのディレクトリへ cd し COMPOSE_PROJECT_NAME / env を再設定する。 | ||
| # これにより任意の CWD からプロジェクトを指定してコンテナ操作できる。 | ||
| # | ||
| # 重要: `build` は shell 実装 (cmd_build) が CWD で動くため、この wrapper cd | ||
| # だけが build の name 解決手段になる (PLAN06 方針 A の核心)。Python 側 chdir | ||
| # フォールバックでは build を救えない。 | ||
| # | ||
| # <name> 判定は projects/ 配下の実在性で行う。これにより `login <index>` / | ||
| # `build <image>` / `scale <N>` の既存 positional と曖昧にならない: 実在する | ||
| # プロジェクト名のときだけ name として解釈し cd + strip する。実在しなければ | ||
| # 引数はそのまま下流 (Python パーサ) へ渡し、Python 側で index/image/scale | ||
| # あるいは「存在しない name」エラーとして扱わせる。 | ||
| # name 候補を受け取り projects/ 配下に実在すれば cd + env 再設定して 0 を返す。 | ||
| maybe_cd_project() { | ||
| local name="${1:-}" | ||
| case "$name" in -*|"") return 1 ;; esac # フラグ・空は name ではない | ||
| local target="${DEVBASE_ROOT}/projects/${name}" | ||
| [ -d "$target" ] || return 1 | ||
| cd "$target" || return 1 | ||
| export COMPOSE_PROJECT_NAME="$name" | ||
| # cd 後にプロジェクトの env を再 source (初期 CWD で読んだ値を上書き)。 | ||
| # project の .env (dotfile) は CRLF / 特殊文字対策で意図的に source しない | ||
| # 方針を踏襲する (冒頭コメント参照)。 | ||
| # | ||
| # 注意: env は環境変数定義のみを想定したファイルであり subshell ではなく | ||
| # 現プロセスで source する。これは wrapper 冒頭 L23-24 の env 読み込みと同一 | ||
| # 意図 — set -a で export した変数を後続の run_python / cmd_build へ引き継ぐ | ||
| # ため、変数が親プロセスに残らない subshell 化はできない。代償として env 内に | ||
| # `exit` 等があると wrapper ごと終了するが、env は (a) プロジェクト所有者が | ||
| # 管理する信頼境界内のファイルで (b) 元々 L24 で初期 CWD でも source される | ||
| # ため、ここで新たなリスクが増えるわけではない。万一 exit を含む env を読んで | ||
| # も「該当プロジェクトの操作が中断する」だけで他プロジェクトへ波及しない。 | ||
| [ -f "env" ] && set -a && source ./env && set +a | ||
| return 0 | ||
| } | ||
| # Resolve the command (skip flags like --version, -V, -h, --help) | ||
| _resolved_cmd="${1:-}" | ||
| case "$_resolved_cmd" in | ||
| --*|-*|"") ;; # flags and empty: don't resolve | ||
| *) _resolved_cmd="$(resolve_command "$_resolved_cmd")" ;; | ||
| esac | ||
| # name 解決: 実在するプロジェクト名を検出したら cd し、その token を argv から | ||
| # 取り除いた配列 _DEVBASE_ARGS を組み立てる。検出しなければ素通し。 | ||
| # name 候補の位置: | ||
| # project|container <sub> <name> -> $3 (サブコマンドは保持) | ||
| # トップレベルシノニム <sub> <name> -> $2 | ||
| # | ||
| # 重要 (PLAN06 codex 指摘対応): `project`/`container` グループでは parser が | ||
| # `name` positional を持つサブコマンド (`up`/`down`/`ps`/`logs`/`scale`) に限定 | ||
| # して $3 を name 解決する。`project login` / `project build` は単一 positional が | ||
| # index / image (旧 container 互換) であり parser が name を受け付けない | ||
| # (cli.py の _add_login_subparser / _add_build_subparser 参照)。これらで $3 を | ||
| # name strip すると、`project build web` の image=web や `project login web` の | ||
| # index 引数が実在プロジェクト名と一致した瞬間に消えて別操作へ化けるため除外する。 | ||
| # | ||
| # トップレベルシノニム (`build`/`login` を含む) は従来どおり「実在 project なら | ||
| # cd」方針を維持する: トップレベル `build`/`login` は Python parser を経由せず | ||
| # shell cmd_build / wrapper cd だけが name 指定の手段であり、`build carmo` / | ||
| # `login carmo` を「そのプロジェクトを操作」と解釈する設計 (存在性ベース判定)。 | ||
| _DEVBASE_ARGS=("${@:2}") | ||
| # 同期注意 (メンテナンス性): 下記 2 リストは cli.py の parser 定義に対応する。 | ||
| # _PROJECT_NAME_SUBCOMMANDS = `project`/`container` で `name` positional を | ||
| # 受け付けるサブコマンド集合。cli.py の _add_project_parser で | ||
| # `add_argument('name', ...)` を持つもの (up/down/ps/logs/scale) と一致させる。 | ||
| # login/build は index/image 互換のため意図的に除外 (上のコメント参照)。 | ||
| # _NAME_RESOLVABLE_SHORTCUTS = トップレベルシノニムのうち「実在 project なら cd」 | ||
| # を許すもの。cli.py の SHORTCUTS 経由で project サブコマンドへ写像される | ||
| # 集合 + shell 実装の build を含む。 | ||
| # cli.py 側でサブコマンドを追加/削除した際は両リストの更新漏れに注意すること | ||
| # (cli.py の _add_project_parser / SHORTCUTS にも対の注記あり)。 | ||
| _PROJECT_NAME_SUBCOMMANDS=" up down ps logs scale " | ||
takemi-ohama marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| _NAME_RESOLVABLE_SHORTCUTS=" up down ps scale login build " | ||
| case "$_resolved_cmd" in | ||
| project|container) | ||
| if [[ "$_PROJECT_NAME_SUBCOMMANDS" == *" ${2:-} "* ]] \ | ||
| && maybe_cd_project "${3:-}"; then | ||
| _DEVBASE_ARGS=("${2:-}" "${@:4}") | ||
| fi | ||
| ;; | ||
| *) | ||
| if [[ "$_NAME_RESOLVABLE_SHORTCUTS" == *" $_resolved_cmd "* ]] \ | ||
| && maybe_cd_project "${2:-}"; then | ||
| _DEVBASE_ARGS=("${@:3}") | ||
| fi | ||
| ;; | ||
| esac | ||
| case "$_resolved_cmd" in | ||
| # Python-implemented commands | ||
| --version|-V) | ||
| run_python "$@" ;; | ||
| init|status|shell-rc|project|container|ct|env|plugin|pl|snapshot|ss|up|down|login|ps|scale) | ||
| run_python "${_resolved_cmd}" "${@:2}" ;; | ||
| run_python "${_resolved_cmd}" "${_DEVBASE_ARGS[@]}" ;; | ||
| # Shell-implemented commands | ||
| build) shift; cmd_build "$@" ;; | ||
| build) cmd_build "${_DEVBASE_ARGS[@]}" ;; | ||
| # Help and unknown | ||
| -h|--help|help|"") run_python "--help" ;; | ||
| *) echo "Error: unknown command '$1'" >&2; exit 1 ;; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -81,33 +81,148 @@ def _run_pre_up_hook() -> bool: | ||
| # ディスパッチャ | ||
| # --------------------------------------------------------------------------- | ||
| def _projects_dir() -> Optional[Path]: | ||
| """$DEVBASE_ROOT/projects を返す。DEVBASE_ROOT 未設定なら None。""" | ||
| root = os.environ.get('DEVBASE_ROOT') | ||
| if not root: | ||
| return None | ||
| return Path(root) / 'projects' | ||
| # 候補一覧に表示するプロジェクト数の上限。多数プロジェクト環境で iterdir 全件を | ||
| # 出力すると 1 行が極端に長くなるため、先頭 N 件 + 「... 他 M 件」で truncate する。 | ||
| _MAX_PROJECT_CANDIDATES = 20 | ||
| def _report_unknown_project(name: str, projects_dir: Path) -> None: | ||
| """存在しない project name に対するエラーと候補一覧を出力する。 | ||
| 候補が多数の場合は先頭 ``_MAX_PROJECT_CANDIDATES`` 件のみ表示し、残りは | ||
| 「... 他 M 件」と省略する。 | ||
| """ | ||
| logger.error("プロジェクト '%s' が見つかりません (%s 配下に存在しません)。", | ||
| name, projects_dir) | ||
| try: | ||
| candidates = sorted( | ||
takemi-ohama marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| p.name for p in projects_dir.iterdir() | ||
| if p.is_dir() or p.is_symlink() | ||
| ) | ||
| except OSError: | ||
| candidates = [] | ||
| if candidates: | ||
| total = len(candidates) | ||
| shown = candidates[:_MAX_PROJECT_CANDIDATES] | ||
| listing = ', '.join(shown) | ||
| if total > _MAX_PROJECT_CANDIDATES: | ||
| listing += f', ... 他 {total - _MAX_PROJECT_CANDIDATES} 件' | ||
| logger.error("利用可能なプロジェクト: %s", listing) | ||
| def _load_project_env(env_file: Path) -> None: | ||
| """プロジェクトの ``env`` ファイルを os.environ へ反映する (wrapper 同等)。 | ||
| wrapper (bin/devbase) は cd 後に ``source ./env`` で env を読み込むため、 | ||
| Python フォールバック経路でも同じ KEY=VALUE を ``os.environ`` に載せて | ||
| 変数欠落 (例: project 固有の ``CONTAINER_SCALE``) を防ぐ。 | ||
| env は環境変数定義のみを想定したファイル (bin/devbase 冒頭コメント参照) の | ||
| ため、ここでは ``export`` 接頭辞付き / 無しの単純な ``KEY=VALUE`` 行のみを | ||
| 解釈する。``#`` コメント・空行は無視し、値の前後のクォートは除去する。shell | ||
| の変数展開やコマンド置換は意図的にサポートしない (安全側に倒す)。 | ||
| """ | ||
| if not env_file.is_file(): | ||
| return | ||
| try: | ||
| lines = env_file.read_text().splitlines() | ||
| except OSError as e: | ||
| logger.warning("env ファイルを読み込めませんでした (%s): %s", env_file, e) | ||
| return | ||
| for raw in lines: | ||
| line = raw.strip() | ||
| if not line or line.startswith('#'): | ||
| continue | ||
| if line.startswith('export '): | ||
| line = line[len('export '):].lstrip() | ||
| if '=' not in line: | ||
| continue | ||
| key, value = line.split('=', 1) | ||
| key = key.strip() | ||
| if not key: | ||
| continue | ||
| value = value.strip() | ||
| if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"): | ||
| value = value[1:-1] | ||
| os.environ[key] = value | ||
| def _resolve_project_name(project_name: str) -> bool: | ||
takemi-ohama marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| """project name を $DEVBASE_ROOT/projects/<name> へ解決し chdir する。 | ||
| 通常は wrapper (bin/devbase) が起動前に cd 済みのため、ここは | ||
| - `python -m devbase.cli project up <name>` の直接起動 | ||
| - wrapper を経ない経路 (`_ensure_env_files` 等) | ||
| に対する防御的フォールバックとして働く。wrapper が既に対象ディレクトリへ | ||
| cd 済みなら chdir は no-op になる (同一パス判定)。 | ||
| chdir 後は wrapper の ``source ./env`` と同等に project の ``env`` を | ||
| ``os.environ`` へ反映し、wrapper を経ない直接起動でも環境変数が欠落しない | ||
| ようにする (gemini round2 minor 指摘対応)。 | ||
| Returns: | ||
| True: 解決成功 (または既に対象ディレクトリにいる) | ||
| False: DEVBASE_ROOT 未設定 / 対象が存在しない (呼び出し側で return 1) | ||
| """ | ||
| projects_dir = _projects_dir() | ||
| if projects_dir is None: | ||
| logger.error("DEVBASE_ROOT が未設定のため project name '%s' を解決できません。", | ||
| project_name) | ||
| return False | ||
| target = projects_dir / project_name | ||
| if not target.is_dir(): | ||
| _report_unknown_project(project_name, projects_dir) | ||
| return False | ||
| try: | ||
| already_there = target.resolve() == Path.cwd().resolve() | ||
| except OSError: | ||
| already_there = False | ||
| if not already_there: | ||
| os.chdir(target) | ||
| # wrapper の `source ./env` と同等に project env を os.environ へ反映する。 | ||
| # wrapper 経由なら既に同じ値が載っているため冪等。 | ||
| _load_project_env(Path('env')) | ||
| # COMPOSE_PROJECT_NAME を name で上書き (wrapper が設定済みでも冪等)。 | ||
| # env 由来の COMPOSE_PROJECT_NAME より name 指定を優先するため env 反映後に行う。 | ||
| os.environ['COMPOSE_PROJECT_NAME'] = project_name | ||
| return True | ||
| def _dispatch_lifecycle(args) -> int: | ||
| """`project` / `container` 共有のサブコマンドディスパッチャ。 | ||
| `project <sub> [name]` の `name` を解決して project_name へ畳み込む。 | ||
| `container` 経路には `name` 属性が無いため従来通り None になる。 | ||
| NOTE (PLAN06): name によるディレクトリ解決の本体は Task 2 (PR2) で wrapper の | ||
| cd + Python フォールバックとして実装する。PR1 では project_name 引数を取れる | ||
| up / scale にのみ name を伝播するが、その name も compose のプロジェクトラベル | ||
| (COMPOSE_PROJECT_NAME 相当) として使われるだけで、操作対象はあくまで CWD の | ||
| compose.yml である点に注意 (ディレクトリ解決は未実装)。 | ||
| name 指定時は handler 呼び出し前に一括で `$DEVBASE_ROOT/projects/<name>` へ | ||
| chdir する (PLAN06 方針 A の Python 側フォールバック)。chdir を各 handler に | ||
| 散らさずここで実施するのは、`cmd_down()` / `cmd_login()` / `cmd_logs()` 等が | ||
| project_name 引数を取らず、per-handler 実装では down/login/logs で名前解決が | ||
| 効かなくなるため。build は wrapper の shell 実装で CWD 実行されるため、この | ||
| Python フォールバックの対象外 (name 属性も持たない)。 | ||
| """ | ||
| subcmd = getattr(args, 'subcommand', None) | ||
| project_name = getattr(args, 'name', None) or getattr(args, 'project_name', None) | ||
| # PR1 では name によるディレクトリ解決は未実装で、どのサブコマンドも CWD の | ||
| # compose.yml に対して動作する。name を指定されたまま黙って CWD に作用すると | ||
| # 「指定したプロジェクトに対して操作できた」と誤解させるため、明示的に警告する | ||
| # (name → ディレクトリ解決は PR2 で実装)。up / scale は name をプロジェクト | ||
| # ラベルには反映するが、対象ディレクトリは依然 CWD であるため同様に警告する。 | ||
| # name 指定時はディレクトリを解決して chdir する。解決失敗 (DEVBASE_ROOT 未設定 | ||
| # / 存在しない name) は候補提示の上でエラー終了する。 | ||
| if project_name: | ||
| logger.warning( | ||
| "project name '%s' によるディレクトリ解決は未実装です。" | ||
| "カレントディレクトリの compose に対して実行します " | ||
| "(name 指定は将来のリリースで対応予定)。", | ||
| project_name, | ||
| ) | ||
| if not _resolve_project_name(project_name): | ||
| return 1 | ||
| handlers = { | ||
| 'up': lambda: cmd_up(project_name=project_name, | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.