Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ __pycache__/
.gemini/
.docker-compose.scale.yml
plugins.yml
repos/
plugins/*/
!plugins/.gitkeep
projects/*
Expand Down
18 changes: 18 additions & 0 deletions docs/user/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,24 @@ devbase plugin info <name>
devbase plugin sync
```

### `devbase plugin migrate`

旧形式 (`plugins/<name>` へのコピー) でインストールされたプラグインを、`repos/` 配下の永続クローンへ移行します。`install` / `update` 実行時にも自動で呼び出されるため、通常は手動実行不要です。

```
devbase plugin migrate
```

移行の挙動:

| 状況 | 動作 |
|---|---|
| コピーがクローンと一致 | 旧コピーを削除し `repos/` へ移行 (migrated) |
| コピーにローカル変更あり | 旧コピーを `plugins/<name>.bak` として保全 (preserved、手動で reconcile) |
| 移行できない (ソース未登録 等) | スキップしてエラーを表示 (skipped) |

`--link` でインストールしたプラグインは移行対象外です。

### `devbase plugin repo add`

プラグインリポジトリを登録します。
Expand Down
7 changes: 6 additions & 1 deletion lib/devbase/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
SUBCMD_MAP = {
('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'],
('plugin', 'pl'): ['list', 'install', 'uninstall', 'update', 'info', 'sync', 'repo', 'migrate'],
('snapshot', 'ss'): ['create', 'list', 'restore', 'copy', 'delete', 'rotate'],
}

Expand Down Expand Up @@ -234,6 +234,9 @@ def _add_plugin_parser(subparsers):

pl_sub.add_parser('sync', help='Resync project symlinks')

pl_sub.add_parser('migrate',
help='Migrate legacy plugins/ installs to repos/ clones')

# Plugin repo sub-subcommands
pl_repo = pl_sub.add_parser('repo', help='Manage plugin repositories')
pl_repo_sub = pl_repo.add_subparsers(dest='repo_command')
Expand All @@ -244,6 +247,8 @@ def _add_plugin_parser(subparsers):

r_remove = pl_repo_sub.add_parser('remove', help='Unregister a repository')
r_remove.add_argument('name', help='Repository name')
r_remove.add_argument('--force', action='store_true',
help='Force removal even if repo has uncommitted/unpushed changes')

pl_repo_sub.add_parser('list', help='List repositories')

Expand Down
41 changes: 39 additions & 2 deletions lib/devbase/commands/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from devbase.plugin.updater import update_plugin
from devbase.plugin.info import show_plugin_info, show_available_plugins
from devbase.plugin.syncer import sync_projects
from devbase.plugin.migrator import migrate
from devbase.plugin.repo_manager import (
add_repository,
remove_repository,
Expand All @@ -34,6 +35,7 @@ def cmd_plugin(devbase_root: Path, args) -> int:
'update': lambda: cmd_plugin_update(devbase_root, getattr(args, 'name', None)),
'info': lambda: cmd_plugin_info(devbase_root, getattr(args, 'name', '')),
'sync': lambda: cmd_sync(devbase_root),
'migrate': lambda: cmd_plugin_migrate(devbase_root),
'repo': lambda: cmd_repo(devbase_root, args),
}

Expand Down Expand Up @@ -138,6 +140,36 @@ def cmd_sync(devbase_root: Path) -> int:
return 0


def cmd_plugin_migrate(devbase_root: Path) -> int:
"""Migrate legacy plugins/ copy installs to repos/ persistent clones"""
registry = PluginRegistry(devbase_root)
try:
result = migrate(registry)
except DevbaseError as e:
logger.error("%s", e)
return 1

if not (result.migrated or result.preserved or result.skipped):
logger.info("No legacy plugins/ installs to migrate.")
return 0

if result.migrated:
logger.info("Migrated %d plugin(s) to repos/: %s",
len(result.migrated), ", ".join(result.migrated))
if result.preserved:
logger.warning(
"Preserved %d plugin(s) with local changes as plugins/<name>.bak "
"(reconcile manually): %s",
len(result.preserved), ", ".join(result.preserved))
if result.skipped:
logger.warning("Could not migrate %d plugin(s): %s",
len(result.skipped), ", ".join(result.skipped))
for err in result.errors:
logger.warning(" %s", err)
return 1
return 0


def cmd_repo(devbase_root: Path, args) -> int:
"""Dispatch repo subcommands"""
registry = PluginRegistry(devbase_root)
Expand All @@ -149,7 +181,8 @@ def cmd_repo(devbase_root: Path, args) -> int:

handlers = {
'add': lambda: add_repository(registry, args.url, name=args.name),
'remove': lambda: remove_repository(registry, args.name),
'remove': lambda: remove_repository(registry, args.name,
force=getattr(args, 'force', False)),
'list': lambda: show_repositories(registry),
'refresh': lambda: _repo_refresh(registry, args),
}
Expand Down Expand Up @@ -181,9 +214,13 @@ def _repo_refresh(registry, args):
errors = []
for repo in repos:
try:
refresh_repository(registry, repo.name)
refresh_repository(registry, repo.name, sync=False)
except DevbaseError as e:
logger.error("%s", e)
errors.append(str(e))

# Sync once after all repos are refreshed (instead of per-repo)
sync_projects(registry)

if errors:
raise DevbaseError(f"{len(errors)} repository refresh(es) failed")
11 changes: 9 additions & 2 deletions lib/devbase/commands/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,17 @@ def _get_plugin_info(registry: PluginRegistry) -> list[dict]:
"""インストール済みプラグインとプロジェクト数を取得する"""
results = []
plugins = registry.list_installed()
plugins_dir = registry.get_plugins_dir()

for plugin in plugins:
plugin_projects_dir = plugins_dir / plugin.name / "projects"
# plugin.path は devbase_root からの相対パス。
# repos/ ベース (repos/<repo>/<subdir>) と --link ベース
# (plugins/<name>) の両方を同じロジックで解決する。
# path が空の場合 (旧/破損エントリ) は devbase_root/projects を
# 誤参照してしまうため、先にガードして 0 件扱いとする。
if not plugin.path:
results.append({"name": plugin.name, "project_count": 0})
continue
plugin_projects_dir = registry.devbase_root / plugin.path / "projects"
Comment thread
takemi-ohama marked this conversation as resolved.
if plugin_projects_dir.is_dir():
project_count = sum(
1 for p in plugin_projects_dir.iterdir() if p.is_dir()
Expand Down
Loading
Loading