Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 92 additions & 4 deletions bin/devbase
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ run_python() {
# Resolve abbreviated command to full command name via unique prefix matching
resolve_command() {
local input="$1"
local commands="init status shell-rc container ct env plugin pl snapshot ss up down login build ps scale help"
local commands="init status shell-rc project container ct env plugin pl snapshot ss up down login build ps scale help"
local matches=()
for cmd in $commands; do
[[ "$cmd" == "$input"* ]] && matches+=("$cmd")
Expand All @@ -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 "
_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|container|ct|env|plugin|pl|snapshot|ss|up|down|login|ps|scale)
run_python "${_resolved_cmd}" "${@:2}" ;;
init|status|shell-rc|project|container|ct|env|plugin|pl|snapshot|ss|up|down|login|ps|scale)
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 ;;
Expand Down
172 changes: 142 additions & 30 deletions lib/devbase/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,24 @@

logger = get_logger("devbase.cli")

# Shortcuts: top-level command -> (group, subcommand)
# Shortcuts: top-level command -> project subcommand
# 委譲先は共有の cmd_project (PLAN06 で container は非推奨化)。
# NOTE: `build` はここに含めない。配布入口 bin/devbase が `build` を shell の
# cmd_build (devbase-base 依存検出 + 2 段ビルド + --no-cache 対応) に委譲しており、
# Python の project build (単純な compose build) とは実装が異なるため。Python 側で
# `build` を project build ショートカットとして広告すると wrapper の実経路と乖離する。
# project build / container build サブコマンド自体は引き続き利用可能。
#
# 同期注意 (メンテナンス性): SHORTCUTS のキー集合と _add_project_parser の
# `name` positional 付きサブコマンドは bin/devbase の _NAME_RESOLVABLE_SHORTCUTS /
# _PROJECT_NAME_SUBCOMMANDS と対応している。サブコマンドを追加/削除する際は
# wrapper 側 (bin/devbase の該当リスト) の更新漏れに注意すること。
SHORTCUTS = {
'up': ('container', 'up'),
'down': ('container', 'down'),
'login': ('container', 'login'),
'build': ('container', 'build'),
'ps': ('container', 'ps'),
'scale': ('container', 'scale'),
'up': 'up',
'down': 'down',
'login': 'login',
'ps': 'ps',
'scale': 'scale',
}

# Group aliases
Expand All @@ -35,6 +45,7 @@

# Subcommand map for prefix resolution: {(aliases...): [subcmds]}
SUBCMD_MAP = {
('project',): ['up', 'down', 'ps', 'login', 'logs', 'scale', 'build'],
('container', 'ct'): ['up', 'down', 'ps', 'login', 'logs', 'scale', 'build'],
('env',): ['init', 'sync', 'list', 'set', 'get', 'delete', 'edit', 'project', 'export', 'import'],
('plugin', 'pl'): ['list', 'install', 'uninstall', 'update', 'info', 'sync', 'repo', 'migrate'],
Expand Down Expand Up @@ -63,6 +74,30 @@ def _require_devbase_root() -> Path:
return Path(root)


def _add_login_subparser(sub):
"""`login` サブコマンドを登録する (project / container 共通)。

単一 positional `index` の意味は両グループで完全に同一。`[name]` を足すと
`project login 2` を name='2' と誤解釈して index=1 にログインしてしまう曖昧さ
(旧 `container login <index>` との非互換) が生じるため、project でも name を
受け付けない。PR2 で project name 解決を導入する際は曖昧さのない `--name`
オプションで対応する方針。
"""
p = sub.add_parser('login', help='Login to container')
p.add_argument('index', nargs='?', default='1', help='Container index')


def _add_build_subparser(sub):
"""`build` サブコマンドを登録する (project / container 共通)。

単一 positional `image` の意味は両グループで同一。`[name]` を許すと
`project build web` が name='web', image=None となり image 指定ビルドが
compose build に化けるため、project でも name を受け付けない (login 参照)。
"""
p = sub.add_parser('build', help='Build container images')
p.add_argument('image', nargs='?', default=None, help='Image name')


def _add_container_parser(subparsers):
"""Container group parser"""
ct_parser = subparsers.add_parser('container', aliases=['ct'],
Expand All @@ -72,8 +107,7 @@ def _add_container_parser(subparsers):
ct_sub.add_parser('up', help='Start containers')
ct_sub.add_parser('down', help='Stop and remove containers')

ct_login = ct_sub.add_parser('login', help='Login to container')
ct_login.add_argument('index', nargs='?', default='1', help='Container index')
_add_login_subparser(ct_sub)

ct_ps = ct_sub.add_parser('ps', help='Show container status')
ct_ps.add_argument('--all', '-a', action='store_true', help='Show all containers')
Expand All @@ -85,8 +119,53 @@ def _add_container_parser(subparsers):
ct_scale = ct_sub.add_parser('scale', help='Scale containers online')
ct_scale.add_argument('new_scale', type=int, help='New number of containers')

ct_build = ct_sub.add_parser('build', help='Build container images')
ct_build.add_argument('image', nargs='?', default=None, help='Image name')
_add_build_subparser(ct_sub)


def _add_project_parser(subparsers):
"""Project group parser (CWD 非依存のプロジェクト操作)。

`container` と同じ subcommand 群に、省略可能な `[name]` positional を加える。
name によるディレクトリ解決 / COMPOSE_PROJECT_NAME 上書きは PLAN06 Task 2 (PR2)
で wrapper の cd + Python フォールバックとして実装する。PR1 では parser 構造と
name のパースまでを用意する。

例外: `login` / `build` は単一 positional が旧 `container` と同義 (index / image)
であり、`[name]` を足すと `project login 2` / `project build web` が誤解釈される
ため name を受け付けない。両者は project / container で定義が完全に一致するので
`_add_login_subparser` / `_add_build_subparser` に共通化している。

同期注意: ここで `name` positional を持つサブコマンド集合 (up/down/ps/logs/scale)
は bin/devbase の `_PROJECT_NAME_SUBCOMMANDS` と一致させる必要がある。追加/削除時は
wrapper 側リストの更新漏れに注意すること。
"""
pj_parser = subparsers.add_parser('project', help='Manage projects (CWD-independent)')
pj_sub = pj_parser.add_subparsers(dest='subcommand')

pj_up = pj_sub.add_parser('up', help='Start containers')
pj_up.add_argument('name', nargs='?', default=None, help='Project name')

pj_down = pj_sub.add_parser('down', help='Stop and remove containers')
pj_down.add_argument('name', nargs='?', default=None, help='Project name')

_add_login_subparser(pj_sub)

pj_ps = pj_sub.add_parser('ps', help='Show container status')
pj_ps.add_argument('name', nargs='?', default=None, help='Project name')
pj_ps.add_argument('--all', '-a', action='store_true', help='Show all containers')

pj_logs = pj_sub.add_parser('logs', help='Show container logs')
pj_logs.add_argument('name', nargs='?', default=None, help='Project name')
pj_logs.add_argument('--follow', '-f', action='store_true', help='Follow log output')
pj_logs.add_argument('--tail', type=int, default=None, help='Number of lines')

# NOTE: `[name]` optional + `new_scale` 必須 int の順。値が 1 個なら new_scale に、
# 2 個なら (name, new_scale) に割り当てられ曖昧にならない (tests/cli 参照)。
pj_scale = pj_sub.add_parser('scale', help='Scale containers online')
pj_scale.add_argument('name', nargs='?', default=None, help='Project name')
pj_scale.add_argument('new_scale', type=int, help='New number of containers')

_add_build_subparser(pj_sub)


def _add_env_parser(subparsers):
Expand Down Expand Up @@ -285,20 +364,36 @@ def _add_snapshot_parser(subparsers):


def _add_shortcuts(subparsers):
"""Top-level shortcut parsers"""
"""Top-level shortcut parsers.

委譲先の `project` サブコマンドと引数体系を揃えるため、`up` / `down` / `ps` /
`scale` は `project <sub> [name]` と同じく省略可能な `[name]` positional を
受け付ける (`devbase up carmo` ≡ `devbase project up carmo`)。受理した name は
_dispatch でショートカット経由でも下流 (cmd_project → _dispatch_lifecycle) へ
伝播する。name の実解決は PLAN06 Task 2 (PR2) で実装するため、PR1 では up/scale
も含め name 指定時に未対応 warning を出す (container.py 参照)。

`login` は project login と同様に単一 positional を `index` として扱い `[name]`
は受け付けない (曖昧さ回避)。`build` はショートカットに含めない (SHORTCUTS の
注記参照): bin/devbase が build を shell 実装 (cmd_build) に委譲するため、
Python 側でトップレベル build を広告すると実経路と乖離する。
"""
login_sc = subparsers.add_parser('login', help='Login to container')
login_sc.add_argument('index', nargs='?', default='1', help='Container index')

build_sc = subparsers.add_parser('build', help='Build container images')
build_sc.add_argument('image', nargs='?', default=None, help='Image name')

ps_sc = subparsers.add_parser('ps', help='Show container status')
ps_sc.add_argument('name', nargs='?', default=None, help='Project name')
ps_sc.add_argument('--all', '-a', action='store_true', help='Show all containers')

subparsers.add_parser('up', help='Start containers')
subparsers.add_parser('down', help='Stop and remove containers')
up_sc = subparsers.add_parser('up', help='Start containers')
up_sc.add_argument('name', nargs='?', default=None, help='Project name')

down_sc = subparsers.add_parser('down', help='Stop and remove containers')
down_sc.add_argument('name', nargs='?', default=None, help='Project name')

# `[name]` optional + `new_scale` 必須 int の順 (project scale と同じ規則)。
scale_sc = subparsers.add_parser('scale', help='Scale containers online')
scale_sc.add_argument('name', nargs='?', default=None, help='Project name')
scale_sc.add_argument('new_scale', type=int, help='New number of containers')


Expand All @@ -310,12 +405,13 @@ def _create_parser():
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Shortcuts:\n"
" up container up\n"
" down container down\n"
" login container login\n"
" build container build\n"
" ps container ps\n"
" scale container scale\n"
" up project up\n"
" down project down\n"
" login project login\n"
" ps project ps\n"
" scale project scale\n"
"\n"
"Note: `container` is deprecated; use `project` instead.\n"
)
)

Expand All @@ -342,6 +438,7 @@ def _create_parser():
help='Print shell RC file path (e.g. source "$(devbase shell-rc)")'
)

_add_project_parser(subparsers)
_add_container_parser(subparsers)
_add_env_parser(subparsers)
_add_plugin_parser(subparsers)
Expand Down Expand Up @@ -371,8 +468,16 @@ def _resolve_prefix(input_cmd, candidates, preferences=None):

def _expand_argv():
"""Expand abbreviated command/subcommand names in sys.argv in-place."""
commands = ['init', 'status', 'shell-rc', 'container', 'ct', 'env', 'plugin', 'pl',
'snapshot', 'ss', 'up', 'down', 'login', 'build', 'ps', 'scale', 'help']
# この `commands` リストの並びは _create_parser のグループ登録順と一致させる:
# トップレベル → グループ (各 group の直後にその alias を隣接配置: container/ct,
# plugin/pl, snapshot/ss) → ショートカット。`project` (推奨) を `container`
# (非推奨) より前に置くのは登録順と揃えた意図的な並びで、prefix 解決は
# _resolve_prefix が一意一致のみ採用するため順序に機能的影響はない。
# `build` はトップレベルショートカットから除外 (SHORTCUTS の注記参照)。
# bin/devbase が build を shell 実装に委譲するため Python 側には top-level
# build parser が無い。project build / container build は引き続き利用可能。
commands = ['init', 'status', 'shell-rc', 'project', 'container', 'ct', 'env', 'plugin', 'pl',
'snapshot', 'ss', 'up', 'down', 'login', 'ps', 'scale', 'help']
repo_subcmds = ['add', 'remove', 'list', 'refresh']

if len(sys.argv) >= 2 and not sys.argv[1].startswith('-'):
Expand Down Expand Up @@ -418,13 +523,20 @@ def _dispatch(cmd, args):
# Resolve group aliases
cmd = GROUP_ALIASES.get(cmd, cmd)

# --- Shortcuts (top-level -> container subcommand) ---
# --- Shortcuts (top-level -> project subcommand) ---
# ショートカットは非推奨ではないため、warning を出す cmd_container ではなく
# 共有の cmd_project へ委譲する。
if cmd in SHORTCUTS:
args.subcommand = SHORTCUTS[cmd][1]
from devbase.commands.container import cmd_container
return cmd_container(args)
args.subcommand = SHORTCUTS[cmd]
from devbase.commands.container import cmd_project
return cmd_project(args)

# --- Project group (推奨) ---
if cmd == 'project':
from devbase.commands.container import cmd_project
return cmd_project(args)

# --- Container group ---
# --- Container group (非推奨: project へ委譲 + warning) ---
if cmd == 'container':
from devbase.commands.container import cmd_container
return cmd_container(args)
Expand Down
Loading
Loading