kapsel-cli 0.5.7__tar.gz → 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/PKG-INFO +1 -1
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/__init__.py +1 -1
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/cli.py +2 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/completer.py +1 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/add.py +204 -25
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/status.py +3 -1
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/upgrade.py +102 -32
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/spec_manager.py +0 -1
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/detector.py +32 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/storage/config.py +6 -2
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/storage/history.py +15 -9
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/banner.py +3 -1
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/card.py +4 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/theme.py +1 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/viewport.py +4 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/PKG-INFO +1 -1
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/pyproject.toml +1 -1
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_add_concurrent.py +2 -1
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_history.py +22 -4
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_ai.py +192 -35
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_fetcher.py +2 -1
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_install.py +17 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_preview.py +2 -2
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/README.md +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/__main__.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/__init__.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/carapace_engine.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/carapace_installer.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/__init__.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/__init__.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/completion.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/config.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/datadir.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/dir_nav.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/help.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/language.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/plugin_switch.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/restart.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/search.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/toggle.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/dispatcher.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/registry.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/__init__.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/block/__init__.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/block/model.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/block/registry.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/block/runner.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/engine.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/executor.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/i18n.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/__init__.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/base.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/catalog.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/context.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/fetcher.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/hooks.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/manager.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/tools/__init__.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/tools/installer.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/tools/registry.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/update_check.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/i18n.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/de/help.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/de/tips.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/en/help.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/en/tips.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/es/help.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/es/tips.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/fr/help.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/fr/tips.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ja/help.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ja/tips.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/kapsel.pot +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ru/help.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ru/tips.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/zh_CN/help.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/zh_CN/tips.yaml +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/storage/__init__.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/storage/logger.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/storage/migrate.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/__init__.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/prompt.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/tips.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/SOURCES.txt +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/dependency_links.txt +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/entry_points.txt +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/requires.txt +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/top_level.txt +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/setup.cfg +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_block_system.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_carapace_installer.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_config.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_dir_nav.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_i18n.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_alias.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_autopilot.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_backup.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_commit.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_help.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_init.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_lang.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_mend.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_portal.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_queue.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_template.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_trans.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_restart_and_pypi.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_search_and_completions.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_spec_manager.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_toggle.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_tools_registry.py +0 -0
- {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_upgrade.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kapsel-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: Next-generation cross-platform terminal capsule & ergonomic shell multiplexer with context-aware command translation and 1,000+ CLI completions.
|
|
5
5
|
Author-email: MrEiu <k648888@vip.qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -104,11 +104,13 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
104
104
|
|
|
105
105
|
normalized = " ".join(stripped.lower().split())
|
|
106
106
|
if normalized in ("exit", "quit", "bye"):
|
|
107
|
+
engine.history_mgr.record_command(command=user_input, mode="builtin")
|
|
107
108
|
os.environ.pop("KAPSEL_ACTIVE", None)
|
|
108
109
|
console.print("[dim]Exited Kapsel.[/]")
|
|
109
110
|
break
|
|
110
111
|
|
|
111
112
|
if normalized in ("restart", "kapsel restart", "reload"):
|
|
113
|
+
engine.history_mgr.record_command(command=user_input, mode="builtin")
|
|
112
114
|
from kapsel.completion.kps.builtins.restart import handle_restart
|
|
113
115
|
return handle_restart(console=console)
|
|
114
116
|
|
|
@@ -312,6 +312,7 @@ class DualStateCompleter(Completer):
|
|
|
312
312
|
("cd", _("Change directory")),
|
|
313
313
|
("clear", _("Clear terminal screen")),
|
|
314
314
|
("exit", _("Exit session")),
|
|
315
|
+
("restart", _("Cleanly restart Kapsel interactive shell session")),
|
|
315
316
|
("git", _("Git version control")),
|
|
316
317
|
("docker", _("Docker container platform")),
|
|
317
318
|
("scoop", _("Windows command-line installer")),
|
|
@@ -26,6 +26,40 @@ _CONFIG_LOCK = threading.Lock()
|
|
|
26
26
|
_CONSOLE_LOCK = threading.Lock()
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _check_plugin_already_installed(
|
|
30
|
+
target_raw: str,
|
|
31
|
+
is_dev: bool = False,
|
|
32
|
+
) -> Tuple[bool, Optional[str], Optional[Path]]:
|
|
33
|
+
"""
|
|
34
|
+
Checks if a plugin is already installed on the system by verifying its directory and version.
|
|
35
|
+
Returns: (is_installed: bool, version: Optional[str], plugin_path: Optional[Path]).
|
|
36
|
+
"""
|
|
37
|
+
has_path_qualifier = any(sep in target_raw for sep in ("/", "\\")) or target_raw.startswith(".")
|
|
38
|
+
if has_path_qualifier:
|
|
39
|
+
p = Path(target_raw).expanduser()
|
|
40
|
+
target_name = p.name.lower()
|
|
41
|
+
else:
|
|
42
|
+
target_name = target_raw.strip().lower()
|
|
43
|
+
|
|
44
|
+
from kapsel.core.plugin.catalog import _extract_plugin_version
|
|
45
|
+
|
|
46
|
+
# 1. Check user global plugins directory (~/.kapsel/plugins/<name>)
|
|
47
|
+
global_plugins_dir = get_kapsel_dir() / "plugins"
|
|
48
|
+
dest_plugin_dir = global_plugins_dir / target_name
|
|
49
|
+
if dest_plugin_dir.is_dir() and ((dest_plugin_dir / "__init__.py").exists() or (dest_plugin_dir / "plugin.py").exists()):
|
|
50
|
+
ver = _extract_plugin_version(dest_plugin_dir)
|
|
51
|
+
return True, ver, dest_plugin_dir
|
|
52
|
+
|
|
53
|
+
# 2. Check local workspace dev directory if in dev mode
|
|
54
|
+
if is_dev:
|
|
55
|
+
dev_plugin_dir = Path.cwd() / "plugins" / target_name
|
|
56
|
+
if dev_plugin_dir.is_dir() and ((dev_plugin_dir / "__init__.py").exists() or (dev_plugin_dir / "plugin.py").exists()):
|
|
57
|
+
ver = _extract_plugin_version(dev_plugin_dir)
|
|
58
|
+
return True, ver, dev_plugin_dir
|
|
59
|
+
|
|
60
|
+
return False, None, None
|
|
61
|
+
|
|
62
|
+
|
|
29
63
|
def _install_single_plugin(
|
|
30
64
|
target_raw: str,
|
|
31
65
|
con: Console,
|
|
@@ -34,12 +68,26 @@ def _install_single_plugin(
|
|
|
34
68
|
) -> Tuple[bool, str]:
|
|
35
69
|
"""
|
|
36
70
|
Installs a single plugin by name or path:
|
|
37
|
-
1.
|
|
38
|
-
2.
|
|
39
|
-
3.
|
|
40
|
-
4.
|
|
71
|
+
1. Checks if the plugin is already installed; if so, skips installation and prompts for upgrade.
|
|
72
|
+
2. Resolves source directory (dev local, user global, or remote fetch).
|
|
73
|
+
3. Copies to user global plugins directory.
|
|
74
|
+
4. Adds to config.yaml enabled plugins list in thread-safe manner.
|
|
75
|
+
5. Triggers declarative tools.yaml installer or legacy install.py.
|
|
41
76
|
Returns (success: bool, message: str).
|
|
42
77
|
"""
|
|
78
|
+
is_installed, cur_ver, existing_dir = _check_plugin_already_installed(target_raw, is_dev=is_dev)
|
|
79
|
+
if is_installed:
|
|
80
|
+
target_name = existing_dir.name.lower() if existing_dir else target_raw.lower()
|
|
81
|
+
if not is_batch:
|
|
82
|
+
with _CONFIG_LOCK:
|
|
83
|
+
cfg = load_config()
|
|
84
|
+
current_enabled = cfg.enabled_plugins or []
|
|
85
|
+
if target_name not in current_enabled:
|
|
86
|
+
new_enabled = current_enabled + [target_name]
|
|
87
|
+
update_config_value("plugins", "enabled", new_enabled)
|
|
88
|
+
ver_str = f" (v{cur_ver})" if cur_ver else ""
|
|
89
|
+
return True, f"Plugin '{target_name}' is already installed{ver_str}. To upgrade this plugin, run: kapsel upgrade {target_name}"
|
|
90
|
+
|
|
43
91
|
global_plugins_dir = get_kapsel_dir() / "plugins"
|
|
44
92
|
global_plugins_dir.mkdir(parents=True, exist_ok=True)
|
|
45
93
|
|
|
@@ -87,13 +135,22 @@ def _install_single_plugin(
|
|
|
87
135
|
con.print(f"[bold #f43f5e]Error:[/] {err}")
|
|
88
136
|
return False, err
|
|
89
137
|
|
|
90
|
-
# Thread-safe config.yaml update
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
138
|
+
# Thread-safe config.yaml update (skip during concurrent batch; batch handler syncs atomically at the end)
|
|
139
|
+
if not is_batch:
|
|
140
|
+
with _CONFIG_LOCK:
|
|
141
|
+
cfg = load_config()
|
|
142
|
+
current_enabled = cfg.enabled_plugins or []
|
|
143
|
+
if target_name not in current_enabled:
|
|
144
|
+
new_enabled = current_enabled + [target_name]
|
|
145
|
+
update_config_value("plugins", "enabled", new_enabled)
|
|
146
|
+
|
|
147
|
+
# Sync Carapace specs for single plugin
|
|
148
|
+
try:
|
|
149
|
+
from kapsel.completion.spec_manager import CarapaceSpecManager
|
|
150
|
+
spec_mgr = CarapaceSpecManager()
|
|
151
|
+
spec_mgr.sync_specs()
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
97
154
|
|
|
98
155
|
# Declarative tool dependency installation via tools.yaml:
|
|
99
156
|
tools_file = plugin_dir / "tools.yaml"
|
|
@@ -147,14 +204,64 @@ def _handle_add_batch_concurrent(
|
|
|
147
204
|
max_workers: int = 4,
|
|
148
205
|
) -> int:
|
|
149
206
|
"""
|
|
150
|
-
Installs multiple plugins concurrently using
|
|
207
|
+
Installs multiple plugins concurrently using an adaptive worker thread pool.
|
|
208
|
+
Dynamically scales thread count when fewer than 4 plugins need installation.
|
|
151
209
|
Synchronizes output and config writing, and displays an aggregated summary table.
|
|
152
210
|
"""
|
|
153
|
-
|
|
211
|
+
cfg = load_config()
|
|
212
|
+
already_enabled = set(p.lower() for p in (cfg.enabled_plugins or []))
|
|
213
|
+
|
|
214
|
+
# Partition targets into uninstalled vs already-installed
|
|
215
|
+
uninstalled: List[str] = []
|
|
216
|
+
already_done: List[str] = []
|
|
217
|
+
for t in targets:
|
|
218
|
+
t_clean = t.strip()
|
|
219
|
+
if not t_clean:
|
|
220
|
+
continue
|
|
221
|
+
is_inst, _, _ = _check_plugin_already_installed(t_clean, is_dev=is_dev)
|
|
222
|
+
if is_inst or t_clean.lower() in already_enabled:
|
|
223
|
+
already_done.append(t_clean)
|
|
224
|
+
else:
|
|
225
|
+
uninstalled.append(t_clean)
|
|
226
|
+
|
|
227
|
+
uninstalled = list(dict.fromkeys(uninstalled))
|
|
228
|
+
already_done = list(dict.fromkeys(already_done))
|
|
229
|
+
|
|
230
|
+
# Ensure already_done are marked enabled in config.yaml
|
|
231
|
+
if already_done:
|
|
232
|
+
with _CONFIG_LOCK:
|
|
233
|
+
cfg = load_config()
|
|
234
|
+
current_enabled = cfg.enabled_plugins or []
|
|
235
|
+
to_enable = [p.lower() for p in already_done if p.lower() not in current_enabled]
|
|
236
|
+
if to_enable:
|
|
237
|
+
update_config_value("plugins", "enabled", current_enabled + to_enable)
|
|
238
|
+
|
|
239
|
+
if not uninstalled:
|
|
240
|
+
con.print(f"\n[bold #10b981]✔ All specified plugins ({len(already_done)}) are already installed.[/]")
|
|
241
|
+
con.print("[dim]Notice: To upgrade installed plugins, please run:[/] [bold white]kapsel upgrade[/]\n")
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
# If only 1 plugin actually needs installation, install directly without worker pool
|
|
245
|
+
if len(uninstalled) == 1:
|
|
246
|
+
if already_done:
|
|
247
|
+
con.print(f"[dim]Notice: {', '.join(already_done)} already installed/enabled.[/]")
|
|
248
|
+
success, msg = _install_single_plugin(uninstalled[0], con, is_dev=is_dev, is_batch=False)
|
|
249
|
+
if success:
|
|
250
|
+
con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
|
|
251
|
+
return 0
|
|
252
|
+
return 1
|
|
253
|
+
|
|
254
|
+
total = len(uninstalled)
|
|
255
|
+
# Dynamically adapt thread count: never spawn more workers than actual uninstalled plugins
|
|
256
|
+
actual_workers = max(1, min(max_workers, total))
|
|
257
|
+
|
|
154
258
|
con.print(f"\n[bold #00f0ff]============================================================[/]")
|
|
155
|
-
con.print(f"[bold #00f0ff] 📦 Kapsel Concurrent Batch Installer ({
|
|
259
|
+
con.print(f"[bold #00f0ff] 📦 Kapsel Concurrent Batch Installer ({actual_workers} Threads, {total} Plugins) [/]")
|
|
156
260
|
con.print(f"[bold #00f0ff]============================================================[/]\n")
|
|
157
261
|
|
|
262
|
+
if already_done:
|
|
263
|
+
con.print(f"[dim]Notice: {len(already_done)} plugin(s) already installed (skipped): {', '.join(already_done)}[/]\n")
|
|
264
|
+
|
|
158
265
|
succeeded: List[str] = []
|
|
159
266
|
failed: List[Tuple[str, str]] = []
|
|
160
267
|
results_lock = threading.Lock()
|
|
@@ -183,8 +290,8 @@ def _handle_add_batch_concurrent(
|
|
|
183
290
|
con.print(f" [bold #f43f5e]✘ [{plugin_name}] Unexpected error:[/] {e}")
|
|
184
291
|
return False, plugin_name, str(e)
|
|
185
292
|
|
|
186
|
-
with ThreadPoolExecutor(max_workers=
|
|
187
|
-
futures = {executor.submit(_worker_task, name): name for name in
|
|
293
|
+
with ThreadPoolExecutor(max_workers=actual_workers) as executor:
|
|
294
|
+
futures = {executor.submit(_worker_task, name): name for name in uninstalled}
|
|
188
295
|
for fut in as_completed(futures):
|
|
189
296
|
ok, name, msg = fut.result()
|
|
190
297
|
with results_lock:
|
|
@@ -197,9 +304,17 @@ def _handle_add_batch_concurrent(
|
|
|
197
304
|
if succeeded:
|
|
198
305
|
with _CONFIG_LOCK:
|
|
199
306
|
cfg = load_config()
|
|
200
|
-
new_enabled = list(dict.fromkeys(cfg.enabled_plugins + succeeded))
|
|
307
|
+
new_enabled = list(dict.fromkeys((cfg.enabled_plugins or []) + succeeded))
|
|
201
308
|
update_config_value("plugins", "enabled", new_enabled)
|
|
202
309
|
|
|
310
|
+
# Sync Carapace specs for newly enabled plugins
|
|
311
|
+
try:
|
|
312
|
+
from kapsel.completion.spec_manager import CarapaceSpecManager
|
|
313
|
+
spec_mgr = CarapaceSpecManager()
|
|
314
|
+
spec_mgr.sync_specs()
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
|
|
203
318
|
# Summary table
|
|
204
319
|
table = Table(
|
|
205
320
|
title=f"[bold #00f0ff]📊 Concurrent Batch Installation Summary ({len(succeeded)}/{total} Succeeded)[/]",
|
|
@@ -209,25 +324,31 @@ def _handle_add_batch_concurrent(
|
|
|
209
324
|
table.add_column("Status", width=24)
|
|
210
325
|
table.add_column("Details", style="dim")
|
|
211
326
|
|
|
212
|
-
for name in
|
|
327
|
+
for name in uninstalled:
|
|
213
328
|
if name in succeeded:
|
|
214
329
|
table.add_row(name, "[bold #10b981]✔ Installed & Enabled[/]", "Ready")
|
|
215
330
|
else:
|
|
216
331
|
err_msg = next((m for (n, m) in failed if n == name), "Failed")
|
|
217
332
|
table.add_row(name, "[bold #f43f5e]✘ Failed[/]", err_msg[:50])
|
|
218
333
|
|
|
334
|
+
for name in already_done:
|
|
335
|
+
table.add_row(name, "[dim #10b981]✔ Already Installed[/]", "Skipped (Use 'kapsel upgrade' to update)")
|
|
336
|
+
|
|
219
337
|
con.print("")
|
|
220
338
|
con.print(table)
|
|
221
339
|
summary_color = "#10b981" if not failed else "#f59e0b"
|
|
222
340
|
con.print(f"\n[{summary_color}]Batch Completed:[/] [white]{len(succeeded)} succeeded[/], "
|
|
223
|
-
f"[{'#f43f5e' if failed else 'white'}]{len(failed)} failed[/]
|
|
341
|
+
f"[{'#f43f5e' if failed else 'white'}]{len(failed)} failed[/], "
|
|
342
|
+
f"[dim]{len(already_done)} already installed[/].\n")
|
|
224
343
|
|
|
225
|
-
return 0 if succeeded else 1
|
|
344
|
+
return 0 if succeeded or (not failed and already_done) else 1
|
|
226
345
|
|
|
227
346
|
|
|
228
347
|
def _handle_add_all(con: Console, is_dev: bool = False, max_workers: int = 4) -> int:
|
|
229
348
|
"""
|
|
230
|
-
Collects all official and dev Kapsel plugins and installs them concurrently
|
|
349
|
+
Collects all uninstalled official and dev Kapsel plugins and installs them concurrently.
|
|
350
|
+
Dynamically scales thread count when fewer than 4 plugins need installation.
|
|
351
|
+
Filters out any already-installed plugins and directs user to 'kapsel upgrade'.
|
|
231
352
|
"""
|
|
232
353
|
from kapsel.core.plugin.catalog import load_plugin_catalog_rich, DEFAULT_CATALOG
|
|
233
354
|
catalog = load_plugin_catalog_rich()
|
|
@@ -247,7 +368,48 @@ def _handle_add_all(con: Console, is_dev: bool = False, max_workers: int = 4) ->
|
|
|
247
368
|
plugin_names.add(item.name.lower())
|
|
248
369
|
|
|
249
370
|
ordered_plugins = sorted(plugin_names)
|
|
250
|
-
|
|
371
|
+
|
|
372
|
+
cfg = load_config()
|
|
373
|
+
already_enabled = set(p.lower() for p in (cfg.enabled_plugins or []))
|
|
374
|
+
|
|
375
|
+
# Check both config.enabled_plugins and actual on-disk installation status with version
|
|
376
|
+
uninstalled: List[str] = []
|
|
377
|
+
already_installed: List[str] = []
|
|
378
|
+
for p in ordered_plugins:
|
|
379
|
+
is_inst, _, _ = _check_plugin_already_installed(p, is_dev=is_dev)
|
|
380
|
+
if is_inst or p.lower() in already_enabled:
|
|
381
|
+
already_installed.append(p)
|
|
382
|
+
else:
|
|
383
|
+
uninstalled.append(p)
|
|
384
|
+
|
|
385
|
+
# Ensure all already_installed are marked enabled in config.yaml
|
|
386
|
+
if already_installed:
|
|
387
|
+
with _CONFIG_LOCK:
|
|
388
|
+
cfg = load_config()
|
|
389
|
+
current_enabled = cfg.enabled_plugins or []
|
|
390
|
+
to_enable = [p.lower() for p in already_installed if p.lower() not in current_enabled]
|
|
391
|
+
if to_enable:
|
|
392
|
+
update_config_value("plugins", "enabled", current_enabled + to_enable)
|
|
393
|
+
|
|
394
|
+
if not uninstalled:
|
|
395
|
+
con.print(f"\n[bold #10b981]✔ All official plugins ({len(already_installed)}) are already installed.[/]")
|
|
396
|
+
con.print("[dim]Notice: To check or upgrade installed plugins, please run:[/] [bold white]kapsel upgrade[/]\n")
|
|
397
|
+
return 0
|
|
398
|
+
|
|
399
|
+
con.print(f"[dim]Skipped {len(already_installed)} already-installed plugin(s). To upgrade, run: kapsel upgrade[/]")
|
|
400
|
+
con.print(f"[dim]Installing {len(uninstalled)} new plugin(s) ({len(ordered_plugins)} total in catalog).[/]\n")
|
|
401
|
+
|
|
402
|
+
# If only 1 uninstalled plugin, no need for thread pool
|
|
403
|
+
if len(uninstalled) == 1:
|
|
404
|
+
success, msg = _install_single_plugin(uninstalled[0], con, is_dev=is_dev, is_batch=False)
|
|
405
|
+
if success:
|
|
406
|
+
con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
|
|
407
|
+
return 0
|
|
408
|
+
return 1
|
|
409
|
+
|
|
410
|
+
# When fewer than max_workers (e.g. 2 or 3), dynamically adapt thread count to len(uninstalled)
|
|
411
|
+
workers = min(max_workers, len(uninstalled))
|
|
412
|
+
return _handle_add_batch_concurrent(uninstalled, con, is_dev=is_dev, max_workers=workers)
|
|
251
413
|
|
|
252
414
|
|
|
253
415
|
def _select_plugins_interactive(con: Console, is_dev: bool = False) -> List[str]:
|
|
@@ -442,7 +604,7 @@ def handle_add_command(args: List[str], console: Optional[Console] = None) -> in
|
|
|
442
604
|
con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
|
|
443
605
|
return 0
|
|
444
606
|
return 1
|
|
445
|
-
return _handle_add_batch_concurrent(chosen, con, is_dev=is_dev, max_workers=4)
|
|
607
|
+
return _handle_add_batch_concurrent(chosen, con, is_dev=is_dev, max_workers=min(4, len(chosen)))
|
|
446
608
|
|
|
447
609
|
# 2. Parse target plugin names from CLI arguments (supporting spaces and commas)
|
|
448
610
|
targets: List[str] = []
|
|
@@ -472,6 +634,23 @@ def handle_add_command(args: List[str], console: Optional[Console] = None) -> in
|
|
|
472
634
|
|
|
473
635
|
# 5. Handle single plugin installation
|
|
474
636
|
if len(targets) == 1:
|
|
637
|
+
target = targets[0]
|
|
638
|
+
has_path_qualifier = any(sep in target for sep in ("/", "\\")) or target.startswith(".")
|
|
639
|
+
target_name = Path(target).name.lower() if has_path_qualifier else target.lower()
|
|
640
|
+
|
|
641
|
+
is_installed, cur_ver, _ = _check_plugin_already_installed(target, is_dev=is_dev)
|
|
642
|
+
if is_installed:
|
|
643
|
+
with _CONFIG_LOCK:
|
|
644
|
+
cfg = load_config()
|
|
645
|
+
current_enabled = cfg.enabled_plugins or []
|
|
646
|
+
if target_name not in current_enabled:
|
|
647
|
+
new_enabled = current_enabled + [target_name]
|
|
648
|
+
update_config_value("plugins", "enabled", new_enabled)
|
|
649
|
+
ver_str = f" (v{cur_ver})" if cur_ver else ""
|
|
650
|
+
con.print(f"\n[bold #f59e0b]●[/] Plugin '[bold #00f0ff]{target_name}[/]' is already installed{ver_str}.")
|
|
651
|
+
con.print(f"[dim]To upgrade this plugin, please run:[/] [bold white]kapsel upgrade {target_name}[/]\n")
|
|
652
|
+
return 0
|
|
653
|
+
|
|
475
654
|
success, msg = _install_single_plugin(targets[0], con, is_dev=is_dev, is_batch=False)
|
|
476
655
|
if success:
|
|
477
656
|
con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
|
|
@@ -479,5 +658,5 @@ def handle_add_command(args: List[str], console: Optional[Console] = None) -> in
|
|
|
479
658
|
else:
|
|
480
659
|
return 1
|
|
481
660
|
|
|
482
|
-
# 6. Handle multiple plugins concurrent installation (
|
|
483
|
-
return _handle_add_batch_concurrent(targets, con, is_dev=is_dev, max_workers=4)
|
|
661
|
+
# 6. Handle multiple plugins concurrent installation (adapted worker count)
|
|
662
|
+
return _handle_add_batch_concurrent(targets, con, is_dev=is_dev, max_workers=min(4, len(targets)))
|
|
@@ -68,12 +68,14 @@ def handle_status(args: Optional[List[str]] = None, console: Optional[Console] =
|
|
|
68
68
|
from kapsel.core.update_check import get_pypi_upgrade_notice
|
|
69
69
|
up_ver = get_pypi_upgrade_notice()
|
|
70
70
|
up_badge = f" [bold #f59e0b]🚀 upgrade (v{up_ver})[/]" if up_ver else ""
|
|
71
|
+
venv = detector.get_virtual_env()
|
|
72
|
+
venv_badge = f" [bold #10b981]({venv})[/]" if venv else ""
|
|
71
73
|
|
|
72
74
|
grid.add_row(
|
|
73
75
|
f"📂 {_('Working Dir:')}",
|
|
74
76
|
f"[dim]{cwd_fmt}[/]",
|
|
75
77
|
f"💊 {_('Kapsel Version:')}",
|
|
76
|
-
f"[bold #00f0ff]v{__version__}[/]{up_badge} (Python {sys.version.split()[0]})",
|
|
78
|
+
f"[bold #00f0ff]v{__version__}[/]{up_badge} (Python {sys.version.split()[0]}){venv_badge}",
|
|
77
79
|
)
|
|
78
80
|
|
|
79
81
|
theme_name = cfg.theme.get("name", "cyber_dark") if isinstance(cfg.theme, dict) else str(cfg.theme)
|
|
@@ -9,6 +9,7 @@ All comments and descriptions are in English.
|
|
|
9
9
|
Default interface language is English with i18n support.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
12
13
|
import importlib.util
|
|
13
14
|
import json
|
|
14
15
|
import os
|
|
@@ -17,6 +18,7 @@ import re
|
|
|
17
18
|
import shutil
|
|
18
19
|
import subprocess
|
|
19
20
|
import sys
|
|
21
|
+
import threading
|
|
20
22
|
from typing import Any, Dict, List, Optional, Tuple
|
|
21
23
|
import urllib.request
|
|
22
24
|
from rich.console import Console
|
|
@@ -35,6 +37,8 @@ from kapsel.ui.banner import ensure_utf8_io
|
|
|
35
37
|
|
|
36
38
|
ensure_utf8_io()
|
|
37
39
|
|
|
40
|
+
_CONSOLE_LOCK = threading.Lock()
|
|
41
|
+
|
|
38
42
|
# Remote endpoints for Kapsel Core releases
|
|
39
43
|
KAPSEL_GITHUB_RELEASE_API = "https://api.github.com/repos/MrEiu/Kapsel/releases/latest"
|
|
40
44
|
KAPSEL_GITHUB_RELEASE_MIRROR_API = "https://ghproxy.net/https://api.github.com/repos/MrEiu/Kapsel/releases/latest"
|
|
@@ -260,28 +264,58 @@ def check_plugins_update(
|
|
|
260
264
|
return results
|
|
261
265
|
|
|
262
266
|
|
|
263
|
-
def
|
|
267
|
+
def install_plugin_dependencies(plugin_dir: Path, con: Console) -> bool:
|
|
264
268
|
"""
|
|
265
|
-
|
|
266
|
-
|
|
269
|
+
Installs missing declarative tools from tools.yaml and/or runs legacy install.py.
|
|
270
|
+
Ensures any uninstalled dependencies are properly installed during upgrade.
|
|
267
271
|
"""
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return True
|
|
272
|
+
all_ok = True
|
|
273
|
+
target_name = plugin_dir.name
|
|
271
274
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
275
|
+
# 1. Declarative tool dependencies via tools.yaml
|
|
276
|
+
tools_file = plugin_dir / "tools.yaml"
|
|
277
|
+
if tools_file.is_file():
|
|
278
|
+
try:
|
|
279
|
+
from kapsel.core.tools.installer import install_plugin_tools
|
|
280
|
+
bin_dir = get_kapsel_dir() / "bin"
|
|
281
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
with _CONSOLE_LOCK:
|
|
283
|
+
con.print(f" [dim]Checking tool dependencies for '[bold #00f0ff]{target_name}[/]' (tools.yaml)...[/]")
|
|
284
|
+
ok = install_plugin_tools(plugin_dir, console=con, bin_dir=bin_dir)
|
|
285
|
+
if not ok:
|
|
286
|
+
all_ok = False
|
|
287
|
+
except Exception as e:
|
|
288
|
+
with _CONSOLE_LOCK:
|
|
289
|
+
con.print(f" [yellow]Warning: Error installing tools.yaml dependencies for '{target_name}':[/] {e}")
|
|
290
|
+
all_ok = False
|
|
291
|
+
|
|
292
|
+
# 2. Legacy install.py script (if present)
|
|
293
|
+
install_script = plugin_dir / "install.py"
|
|
294
|
+
if install_script.is_file():
|
|
295
|
+
try:
|
|
296
|
+
with _CONSOLE_LOCK:
|
|
297
|
+
con.print(f" [dim]Running post-install setup for '[bold #00f0ff]{target_name}[/]' (install.py)...[/]")
|
|
298
|
+
spec = importlib.util.spec_from_file_location(f"kapsel_install_{target_name}", install_script)
|
|
299
|
+
if spec and spec.loader:
|
|
300
|
+
mod = importlib.util.module_from_spec(spec)
|
|
301
|
+
sys.modules[f"kapsel_install_{target_name}"] = mod
|
|
302
|
+
spec.loader.exec_module(mod)
|
|
303
|
+
if hasattr(mod, "install"):
|
|
304
|
+
bin_dir = get_kapsel_dir() / "bin"
|
|
305
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
306
|
+
res = mod.install(con, bin_dir)
|
|
307
|
+
if not res:
|
|
308
|
+
all_ok = False
|
|
309
|
+
except Exception as e:
|
|
310
|
+
with _CONSOLE_LOCK:
|
|
311
|
+
con.print(f" [yellow]Notice: Post-install setup warning for '{target_name}':[/] {e}")
|
|
312
|
+
all_ok = False
|
|
313
|
+
|
|
314
|
+
return all_ok
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# Alias for backward compatibility
|
|
318
|
+
run_plugin_post_install = install_plugin_dependencies
|
|
285
319
|
|
|
286
320
|
|
|
287
321
|
def prompt_user_choice(prompt_msg: str, default: str = "y") -> str:
|
|
@@ -364,7 +398,7 @@ def handle_upgrade(args: List[str], console: Optional[Console] = None) -> int:
|
|
|
364
398
|
dest_dir = info["dir"] or (get_kapsel_dir() / "plugins" / target_plugin)
|
|
365
399
|
con.print(f"[dim]{_('Downloading and updating plugin package...')}[/]")
|
|
366
400
|
if fetch_plugin_from_remote(target_plugin, dest_dir, con):
|
|
367
|
-
|
|
401
|
+
install_plugin_dependencies(dest_dir, con)
|
|
368
402
|
try:
|
|
369
403
|
from kapsel.completion.spec_manager import CarapaceSpecManager
|
|
370
404
|
CarapaceSpecManager().sync_specs()
|
|
@@ -490,17 +524,53 @@ def handle_upgrade(args: List[str], console: Optional[Console] = None) -> int:
|
|
|
490
524
|
con.print(f"[dim]{_('Cancelled.')}[/]\n")
|
|
491
525
|
return 0
|
|
492
526
|
|
|
493
|
-
# Apply plugin updates
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
527
|
+
# Apply plugin updates concurrently
|
|
528
|
+
actual_workers = max(1, min(4, len(upgradable_plugins)))
|
|
529
|
+
con.print(f"\n[bold #00f0ff]━━━ {_('Updating')} {len(upgradable_plugins)} {_('plugin(s) concurrently')} ({actual_workers} {_('Threads')}) ━━━[/]\n")
|
|
530
|
+
|
|
531
|
+
succeeded_plugins: List[str] = []
|
|
532
|
+
failed_plugins: List[Tuple[str, str]] = []
|
|
533
|
+
results_lock = threading.Lock()
|
|
534
|
+
|
|
535
|
+
def _upgrade_worker(p_info: Dict[str, Any]) -> Tuple[bool, str, str]:
|
|
536
|
+
name = p_info["name"]
|
|
537
|
+
latest_ver = p_info["latest"]
|
|
538
|
+
dest_dir = p_info["dir"] or (get_kapsel_dir() / "plugins" / name)
|
|
539
|
+
|
|
540
|
+
with _CONSOLE_LOCK:
|
|
541
|
+
con.print(f"[dim]{_('Updating')} [bold #00f0ff]{name}[/] {_('to')} v{latest_ver}...[/]")
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
if fetch_plugin_from_remote(name, dest_dir, con):
|
|
545
|
+
install_plugin_dependencies(dest_dir, con)
|
|
546
|
+
with _CONSOLE_LOCK:
|
|
547
|
+
con.print(f" [bold #10b981]✔ {_('Updated')} {name} {_('to')} v{latest_ver}[/]")
|
|
548
|
+
return True, name, f"v{latest_ver}"
|
|
549
|
+
else:
|
|
550
|
+
with _CONSOLE_LOCK:
|
|
551
|
+
con.print(f" [bold #f43f5e]✘ {_('Failed to update')} {name}[/]")
|
|
552
|
+
return False, name, "Download failed"
|
|
553
|
+
except Exception as ex:
|
|
554
|
+
with _CONSOLE_LOCK:
|
|
555
|
+
con.print(f" [bold #f43f5e]✘ {_('Error updating')} {name}:[/] {ex}")
|
|
556
|
+
return False, name, str(ex)
|
|
557
|
+
|
|
558
|
+
if len(upgradable_plugins) == 1:
|
|
559
|
+
ok, name, detail = _upgrade_worker(upgradable_plugins[0])
|
|
560
|
+
if ok:
|
|
561
|
+
succeeded_plugins.append(name)
|
|
502
562
|
else:
|
|
503
|
-
|
|
563
|
+
failed_plugins.append((name, detail))
|
|
564
|
+
else:
|
|
565
|
+
with ThreadPoolExecutor(max_workers=actual_workers) as executor:
|
|
566
|
+
futures = {executor.submit(_upgrade_worker, p): p["name"] for p in upgradable_plugins}
|
|
567
|
+
for fut in as_completed(futures):
|
|
568
|
+
ok, name, detail = fut.result()
|
|
569
|
+
with results_lock:
|
|
570
|
+
if ok:
|
|
571
|
+
succeeded_plugins.append(name)
|
|
572
|
+
else:
|
|
573
|
+
failed_plugins.append((name, detail))
|
|
504
574
|
|
|
505
575
|
# Re-sync completion specifications
|
|
506
576
|
try:
|
|
@@ -509,5 +579,5 @@ def handle_upgrade(args: List[str], console: Optional[Console] = None) -> int:
|
|
|
509
579
|
except Exception:
|
|
510
580
|
pass
|
|
511
581
|
|
|
512
|
-
con.print(f"\n[bold #10b981]✨ {_('Successfully updated')} {
|
|
513
|
-
return 0
|
|
582
|
+
con.print(f"\n[bold #10b981]✨ {_('Successfully updated')} {len(succeeded_plugins)}/{len(upgradable_plugins)} {_('plugin(s)!')}[/]\n")
|
|
583
|
+
return 0 if not failed_plugins else 1
|
|
@@ -121,7 +121,6 @@ CORE_SYSTEM_COMMANDS: List[Dict[str, Any]] = [
|
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
"name": "upgrade",
|
|
124
|
-
"aliases": ["update"],
|
|
125
124
|
"description": "Check and upgrade Kapsel Core and official plugins with release notes",
|
|
126
125
|
"flags": {
|
|
127
126
|
"-c, --check": "Check for updates without downloading or installing",
|
|
@@ -158,5 +158,37 @@ class EnvironmentDetector:
|
|
|
158
158
|
except ValueError:
|
|
159
159
|
return str(p)
|
|
160
160
|
|
|
161
|
+
def get_virtual_env(self) -> Optional[str]:
|
|
162
|
+
"""
|
|
163
|
+
Detects active Python virtual environment or Conda environment.
|
|
164
|
+
Returns the environment name (e.g. 'base', 'myenv', '.venv') or None if not active.
|
|
165
|
+
"""
|
|
166
|
+
# 1. Conda environment
|
|
167
|
+
conda_env = os.environ.get("CONDA_DEFAULT_ENV")
|
|
168
|
+
if conda_env:
|
|
169
|
+
clean = conda_env.strip()
|
|
170
|
+
if clean:
|
|
171
|
+
return clean
|
|
172
|
+
|
|
173
|
+
# 2. VIRTUAL_ENV_PROMPT (Python 3.11+ custom venv prompt)
|
|
174
|
+
venv_prompt = os.environ.get("VIRTUAL_ENV_PROMPT")
|
|
175
|
+
if venv_prompt:
|
|
176
|
+
clean = venv_prompt.strip().strip("()").strip()
|
|
177
|
+
if clean:
|
|
178
|
+
return clean
|
|
179
|
+
|
|
180
|
+
# 3. Standard VIRTUAL_ENV path
|
|
181
|
+
virtual_env = os.environ.get("VIRTUAL_ENV")
|
|
182
|
+
if virtual_env:
|
|
183
|
+
name = Path(virtual_env).name.strip()
|
|
184
|
+
if name:
|
|
185
|
+
return name
|
|
186
|
+
|
|
187
|
+
# 4. In-process virtual environment check (sys.prefix != sys.base_prefix)
|
|
188
|
+
if hasattr(sys, "base_prefix") and sys.prefix != sys.base_prefix:
|
|
189
|
+
return Path(sys.prefix).name.strip()
|
|
190
|
+
|
|
191
|
+
return None
|
|
192
|
+
|
|
161
193
|
|
|
162
194
|
detector = EnvironmentDetector()
|
|
@@ -29,6 +29,7 @@ DEFAULT_CONFIG: Dict[str, Any] = {
|
|
|
29
29
|
"enable_card_border": True,
|
|
30
30
|
"show_git_branch": True,
|
|
31
31
|
"show_shell_badge": True,
|
|
32
|
+
"show_venv_badge": True,
|
|
32
33
|
"show_timestamp": True,
|
|
33
34
|
"prompt_symbols": {
|
|
34
35
|
"top": "╭─",
|
|
@@ -58,7 +59,7 @@ DEFAULT_CONFIG: Dict[str, Any] = {
|
|
|
58
59
|
"auto_strip_spaces": True,
|
|
59
60
|
},
|
|
60
61
|
"history": {
|
|
61
|
-
"max_memory_entries":
|
|
62
|
+
"max_memory_entries": 150,
|
|
62
63
|
"frequency_learning": True,
|
|
63
64
|
},
|
|
64
65
|
"cloud": {
|
|
@@ -137,6 +138,9 @@ ui:
|
|
|
137
138
|
# 提示行是否显示当前外层 Shell 徽章 (如 [pwsh], [cmd])
|
|
138
139
|
show_shell_badge: true
|
|
139
140
|
|
|
141
|
+
# 提示行是否显示当前 Python / Conda 虚拟环境徽标 (如 (base), (.venv))
|
|
142
|
+
show_venv_badge: true
|
|
143
|
+
|
|
140
144
|
# 提示行是否显示当前执行时间戳
|
|
141
145
|
show_timestamp: true
|
|
142
146
|
|
|
@@ -271,7 +275,7 @@ class KapselConfig:
|
|
|
271
275
|
|
|
272
276
|
@property
|
|
273
277
|
def history_entries(self) -> int:
|
|
274
|
-
return int(self.raw.get("history", {}).get("max_memory_entries",
|
|
278
|
+
return int(self.raw.get("history", {}).get("max_memory_entries", 150))
|
|
275
279
|
|
|
276
280
|
@property
|
|
277
281
|
def enabled_plugins(self) -> List[str]:
|