kapsel-cli 0.5.4__tar.gz → 0.5.7__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.4 → kapsel_cli-0.5.7}/PKG-INFO +1 -1
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/__init__.py +1 -1
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/cli.py +9 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/__init__.py +10 -0
- kapsel_cli-0.5.7/kapsel/completion/kps/builtins/add.py +483 -0
- kapsel_cli-0.5.7/kapsel/completion/kps/builtins/restart.py +74 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/status.py +5 -1
- kapsel_cli-0.5.7/kapsel/core/tools/__init__.py +36 -0
- kapsel_cli-0.5.7/kapsel/core/tools/installer.py +374 -0
- kapsel_cli-0.5.7/kapsel/core/tools/registry.py +219 -0
- kapsel_cli-0.5.7/kapsel/core/update_check.py +140 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/banner.py +7 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/PKG-INFO +1 -1
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/SOURCES.txt +10 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/pyproject.toml +1 -1
- kapsel_cli-0.5.7/tests/test_add_concurrent.py +81 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_backup.py +1 -1
- kapsel_cli-0.5.7/tests/test_plugin_help.py +204 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_install.py +73 -0
- kapsel_cli-0.5.7/tests/test_plugin_mend.py +154 -0
- kapsel_cli-0.5.7/tests/test_restart_and_pypi.py +85 -0
- kapsel_cli-0.5.7/tests/test_tools_registry.py +87 -0
- kapsel_cli-0.5.4/kapsel/completion/kps/builtins/add.py +0 -218
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/README.md +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/__main__.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/__init__.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/carapace_engine.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/carapace_installer.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/completer.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/__init__.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/completion.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/config.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/datadir.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/dir_nav.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/help.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/language.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/plugin_switch.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/search.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/toggle.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/upgrade.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/dispatcher.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/registry.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/spec_manager.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/__init__.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/block/__init__.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/block/model.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/block/registry.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/block/runner.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/detector.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/engine.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/executor.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/i18n.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/__init__.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/base.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/catalog.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/context.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/fetcher.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/hooks.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/manager.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/i18n.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/de/help.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/de/tips.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/en/help.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/en/tips.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/es/help.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/es/tips.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/fr/help.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/fr/tips.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ja/help.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ja/tips.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/kapsel.pot +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ru/help.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ru/tips.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/zh_CN/help.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/zh_CN/tips.yaml +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/storage/__init__.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/storage/config.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/storage/history.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/storage/logger.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/storage/migrate.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/__init__.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/card.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/prompt.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/theme.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/tips.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/viewport.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/dependency_links.txt +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/entry_points.txt +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/requires.txt +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/top_level.txt +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/setup.cfg +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_block_system.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_carapace_installer.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_config.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_dir_nav.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_history.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_i18n.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_ai.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_alias.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_autopilot.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_commit.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_fetcher.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_init.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_lang.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_portal.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_preview.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_queue.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_template.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_trans.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_search_and_completions.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_spec_manager.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_toggle.py +0 -0
- {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_upgrade.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kapsel-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.7
|
|
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
|
|
@@ -48,6 +48,11 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
48
48
|
)
|
|
49
49
|
return result.execution.exit_code
|
|
50
50
|
|
|
51
|
+
# Handle 'kapsel restart'
|
|
52
|
+
if clean_args and clean_args[0] == "restart":
|
|
53
|
+
from kapsel.completion.kps.builtins.restart import handle_restart
|
|
54
|
+
return handle_restart()
|
|
55
|
+
|
|
51
56
|
# If positional subcommands passed (e.g. kapsel toggle, kapsel status, kapsel help, kapsel add, kapsel config, kapsel datadir)
|
|
52
57
|
if clean_args:
|
|
53
58
|
engine = DualStateEngine()
|
|
@@ -103,6 +108,10 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
103
108
|
console.print("[dim]Exited Kapsel.[/]")
|
|
104
109
|
break
|
|
105
110
|
|
|
111
|
+
if normalized in ("restart", "kapsel restart", "reload"):
|
|
112
|
+
from kapsel.completion.kps.builtins.restart import handle_restart
|
|
113
|
+
return handle_restart(console=console)
|
|
114
|
+
|
|
106
115
|
try:
|
|
107
116
|
result = engine.dispatch(user_input)
|
|
108
117
|
if engine.config.enable_card_border and not result.execution.is_builtin:
|
|
@@ -215,3 +215,13 @@ def register_builtins(registry: "KpsCommandRegistry") -> None:
|
|
|
215
215
|
scope="system",
|
|
216
216
|
)
|
|
217
217
|
|
|
218
|
+
# 12. restart (Re-launch Kapsel interactive shell)
|
|
219
|
+
from kapsel.completion.kps.builtins.restart import handle_restart_command
|
|
220
|
+
registry.register(
|
|
221
|
+
name="restart",
|
|
222
|
+
handler=handle_restart_command,
|
|
223
|
+
help_text="Cleanly restart Kapsel interactive shell session",
|
|
224
|
+
usage="kapsel restart",
|
|
225
|
+
scope="system",
|
|
226
|
+
)
|
|
227
|
+
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kapsel System Command: 'kapsel add <plugin_name...>'.
|
|
3
|
+
Enables and registers plugins into the Kapsel environment.
|
|
4
|
+
Supports 4-thread concurrent installation, multi-selection CLI arguments,
|
|
5
|
+
and interactive checkbox multi-selection menu.
|
|
6
|
+
All comments and descriptions are in English.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
|
+
import importlib.util
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
from typing import List, Optional, Set, Tuple
|
|
16
|
+
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from kapsel.storage.config import get_kapsel_dir, load_config, update_config_value
|
|
22
|
+
from kapsel.ui.banner import ensure_utf8_io
|
|
23
|
+
|
|
24
|
+
# Mutex locks to ensure thread-safe console output and atomic config updates
|
|
25
|
+
_CONFIG_LOCK = threading.Lock()
|
|
26
|
+
_CONSOLE_LOCK = threading.Lock()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _install_single_plugin(
|
|
30
|
+
target_raw: str,
|
|
31
|
+
con: Console,
|
|
32
|
+
is_dev: bool = False,
|
|
33
|
+
is_batch: bool = False,
|
|
34
|
+
) -> Tuple[bool, str]:
|
|
35
|
+
"""
|
|
36
|
+
Installs a single plugin by name or path:
|
|
37
|
+
1. Resolves source directory (dev local, user global, or remote fetch).
|
|
38
|
+
2. Copies to user global plugins directory.
|
|
39
|
+
3. Adds to config.yaml enabled plugins list in thread-safe manner.
|
|
40
|
+
4. Triggers declarative tools.yaml installer or legacy install.py.
|
|
41
|
+
Returns (success: bool, message: str).
|
|
42
|
+
"""
|
|
43
|
+
global_plugins_dir = get_kapsel_dir() / "plugins"
|
|
44
|
+
global_plugins_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
source_path = Path(target_raw).expanduser()
|
|
47
|
+
has_path_qualifier = any(sep in target_raw for sep in ("/", "\\")) or target_raw.startswith(".")
|
|
48
|
+
|
|
49
|
+
if (has_path_qualifier or is_dev) and source_path.exists() and source_path.is_dir():
|
|
50
|
+
target_name = source_path.name.lower()
|
|
51
|
+
dest_plugin_dir = global_plugins_dir / target_name
|
|
52
|
+
|
|
53
|
+
if not (source_path / "__init__.py").exists():
|
|
54
|
+
err = f"Source directory '{source_path}' does not contain '__init__.py'."
|
|
55
|
+
with _CONSOLE_LOCK:
|
|
56
|
+
con.print(f"[bold #f43f5e]Error:[/] {err}")
|
|
57
|
+
return False, err
|
|
58
|
+
|
|
59
|
+
shutil.copytree(source_path, dest_plugin_dir, dirs_exist_ok=True)
|
|
60
|
+
plugin_dir = dest_plugin_dir
|
|
61
|
+
else:
|
|
62
|
+
target_name = target_raw.lower()
|
|
63
|
+
dest_plugin_dir = global_plugins_dir / target_name
|
|
64
|
+
dev_plugin_dir = Path.cwd() / "plugins" / target_name
|
|
65
|
+
|
|
66
|
+
if is_dev and dev_plugin_dir.exists() and dev_plugin_dir.is_dir() and (dev_plugin_dir / "__init__.py").exists():
|
|
67
|
+
shutil.copytree(dev_plugin_dir, dest_plugin_dir, dirs_exist_ok=True)
|
|
68
|
+
plugin_dir = dest_plugin_dir
|
|
69
|
+
elif dest_plugin_dir.exists() and dest_plugin_dir.is_dir():
|
|
70
|
+
plugin_dir = dest_plugin_dir
|
|
71
|
+
else:
|
|
72
|
+
from kapsel.core.plugin.fetcher import fetch_plugin_from_remote
|
|
73
|
+
if fetch_plugin_from_remote(target_name, dest_plugin_dir, con):
|
|
74
|
+
plugin_dir = dest_plugin_dir
|
|
75
|
+
else:
|
|
76
|
+
err = f"Plugin '{target_name}' could not be installed."
|
|
77
|
+
with _CONSOLE_LOCK:
|
|
78
|
+
con.print(f"[bold #f43f5e]Error:[/] {err}")
|
|
79
|
+
if not is_batch:
|
|
80
|
+
con.print(f"[dim]Expected location:[/] {dest_plugin_dir}")
|
|
81
|
+
con.print("[dim]To install a custom plugin, specify its local path or place it into your global plugins directory.[/]\n")
|
|
82
|
+
return False, err
|
|
83
|
+
|
|
84
|
+
if not (plugin_dir / "__init__.py").exists():
|
|
85
|
+
err = f"Plugin directory '{plugin_dir}' does not contain a valid '__init__.py'."
|
|
86
|
+
with _CONSOLE_LOCK:
|
|
87
|
+
con.print(f"[bold #f43f5e]Error:[/] {err}")
|
|
88
|
+
return False, err
|
|
89
|
+
|
|
90
|
+
# Thread-safe config.yaml update
|
|
91
|
+
with _CONFIG_LOCK:
|
|
92
|
+
cfg = load_config()
|
|
93
|
+
current_enabled = cfg.enabled_plugins
|
|
94
|
+
if target_name not in current_enabled:
|
|
95
|
+
new_enabled = current_enabled + [target_name]
|
|
96
|
+
update_config_value("plugins", "enabled", new_enabled)
|
|
97
|
+
|
|
98
|
+
# Declarative tool dependency installation via tools.yaml:
|
|
99
|
+
tools_file = plugin_dir / "tools.yaml"
|
|
100
|
+
if tools_file.is_file():
|
|
101
|
+
try:
|
|
102
|
+
from kapsel.core.tools.installer import install_plugin_tools
|
|
103
|
+
bin_dir = get_kapsel_dir() / "bin"
|
|
104
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
install_plugin_tools(plugin_dir, console=con, bin_dir=bin_dir)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
with _CONSOLE_LOCK:
|
|
108
|
+
con.print(f"[yellow]Warning: Error installing dependencies from tools.yaml for '{target_name}':[/] {e}")
|
|
109
|
+
|
|
110
|
+
# Legacy install.py fallback (if present):
|
|
111
|
+
install_script = plugin_dir / "install.py"
|
|
112
|
+
if install_script.exists():
|
|
113
|
+
try:
|
|
114
|
+
bin_dir = get_kapsel_dir() / "bin"
|
|
115
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
|
|
117
|
+
module_name = f"kapsel_installer_{target_name}"
|
|
118
|
+
spec = importlib.util.spec_from_file_location(module_name, install_script)
|
|
119
|
+
if spec and spec.loader:
|
|
120
|
+
installer_mod = importlib.util.module_from_spec(spec)
|
|
121
|
+
sys.modules[module_name] = installer_mod
|
|
122
|
+
spec.loader.exec_module(installer_mod)
|
|
123
|
+
install_fn = getattr(installer_mod, "install", None)
|
|
124
|
+
if callable(install_fn):
|
|
125
|
+
with _CONSOLE_LOCK:
|
|
126
|
+
con.print(f"[dim]Running legacy installer for '[#00f0ff]{target_name}[/]'...[/]")
|
|
127
|
+
success = install_fn(con, bin_dir)
|
|
128
|
+
if not success:
|
|
129
|
+
with _CONSOLE_LOCK:
|
|
130
|
+
con.print(f"[yellow]Notice: Installation script for '{target_name}' finished with warnings.[/]")
|
|
131
|
+
except Exception as e:
|
|
132
|
+
with _CONSOLE_LOCK:
|
|
133
|
+
con.print(f"[yellow]Warning: Failed to execute legacy installer for '{target_name}':[/] {e}")
|
|
134
|
+
|
|
135
|
+
from kapsel.core.plugin.catalog import _extract_plugin_version
|
|
136
|
+
ver = _extract_plugin_version(plugin_dir)
|
|
137
|
+
|
|
138
|
+
msg = f"[bold #10b981]✔ Plugin '[#00f0ff]{target_name}[/]' (v{ver}) successfully added and enabled![/]\n\n"
|
|
139
|
+
msg += f"[dim]Location: {plugin_dir}[/]"
|
|
140
|
+
return True, msg
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _handle_add_batch_concurrent(
|
|
144
|
+
targets: List[str],
|
|
145
|
+
con: Console,
|
|
146
|
+
is_dev: bool = False,
|
|
147
|
+
max_workers: int = 4,
|
|
148
|
+
) -> int:
|
|
149
|
+
"""
|
|
150
|
+
Installs multiple plugins concurrently using a 4-thread worker pool.
|
|
151
|
+
Synchronizes output and config writing, and displays an aggregated summary table.
|
|
152
|
+
"""
|
|
153
|
+
total = len(targets)
|
|
154
|
+
con.print(f"\n[bold #00f0ff]============================================================[/]")
|
|
155
|
+
con.print(f"[bold #00f0ff] 📦 Kapsel Concurrent Batch Installer ({max_workers} Threads, {total} Plugins) [/]")
|
|
156
|
+
con.print(f"[bold #00f0ff]============================================================[/]\n")
|
|
157
|
+
|
|
158
|
+
succeeded: List[str] = []
|
|
159
|
+
failed: List[Tuple[str, str]] = []
|
|
160
|
+
results_lock = threading.Lock()
|
|
161
|
+
progress_counter = 0
|
|
162
|
+
counter_lock = threading.Lock()
|
|
163
|
+
|
|
164
|
+
def _worker_task(plugin_name: str) -> Tuple[bool, str, str]:
|
|
165
|
+
nonlocal progress_counter
|
|
166
|
+
with counter_lock:
|
|
167
|
+
progress_counter += 1
|
|
168
|
+
idx = progress_counter
|
|
169
|
+
|
|
170
|
+
with _CONSOLE_LOCK:
|
|
171
|
+
con.print(f"[bold #00f0ff]━━━ [{idx}/{total}] Adding & Installing: [white]{plugin_name}[/] ━━━[/]")
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
ok, msg = _install_single_plugin(plugin_name, con, is_dev=is_dev, is_batch=True)
|
|
175
|
+
with _CONSOLE_LOCK:
|
|
176
|
+
if ok:
|
|
177
|
+
con.print(f" [bold #10b981]✔ [{plugin_name}] Added & dependencies verified[/]")
|
|
178
|
+
else:
|
|
179
|
+
con.print(f" [bold #f43f5e]✘ [{plugin_name}] Failed: {msg}[/]")
|
|
180
|
+
return ok, plugin_name, msg
|
|
181
|
+
except Exception as e:
|
|
182
|
+
with _CONSOLE_LOCK:
|
|
183
|
+
con.print(f" [bold #f43f5e]✘ [{plugin_name}] Unexpected error:[/] {e}")
|
|
184
|
+
return False, plugin_name, str(e)
|
|
185
|
+
|
|
186
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
187
|
+
futures = {executor.submit(_worker_task, name): name for name in targets}
|
|
188
|
+
for fut in as_completed(futures):
|
|
189
|
+
ok, name, msg = fut.result()
|
|
190
|
+
with results_lock:
|
|
191
|
+
if ok:
|
|
192
|
+
succeeded.append(name)
|
|
193
|
+
else:
|
|
194
|
+
failed.append((name, msg))
|
|
195
|
+
|
|
196
|
+
# Single atomic batch configuration sync for enabled_plugins
|
|
197
|
+
if succeeded:
|
|
198
|
+
with _CONFIG_LOCK:
|
|
199
|
+
cfg = load_config()
|
|
200
|
+
new_enabled = list(dict.fromkeys(cfg.enabled_plugins + succeeded))
|
|
201
|
+
update_config_value("plugins", "enabled", new_enabled)
|
|
202
|
+
|
|
203
|
+
# Summary table
|
|
204
|
+
table = Table(
|
|
205
|
+
title=f"[bold #00f0ff]📊 Concurrent Batch Installation Summary ({len(succeeded)}/{total} Succeeded)[/]",
|
|
206
|
+
border_style="#0891b2",
|
|
207
|
+
)
|
|
208
|
+
table.add_column("Plugin", style="#00f0ff", width=16)
|
|
209
|
+
table.add_column("Status", width=24)
|
|
210
|
+
table.add_column("Details", style="dim")
|
|
211
|
+
|
|
212
|
+
for name in targets:
|
|
213
|
+
if name in succeeded:
|
|
214
|
+
table.add_row(name, "[bold #10b981]✔ Installed & Enabled[/]", "Ready")
|
|
215
|
+
else:
|
|
216
|
+
err_msg = next((m for (n, m) in failed if n == name), "Failed")
|
|
217
|
+
table.add_row(name, "[bold #f43f5e]✘ Failed[/]", err_msg[:50])
|
|
218
|
+
|
|
219
|
+
con.print("")
|
|
220
|
+
con.print(table)
|
|
221
|
+
summary_color = "#10b981" if not failed else "#f59e0b"
|
|
222
|
+
con.print(f"\n[{summary_color}]Batch Completed:[/] [white]{len(succeeded)} succeeded[/], "
|
|
223
|
+
f"[{'#f43f5e' if failed else 'white'}]{len(failed)} failed[/].\n")
|
|
224
|
+
|
|
225
|
+
return 0 if succeeded else 1
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _handle_add_all(con: Console, is_dev: bool = False, max_workers: int = 4) -> int:
|
|
229
|
+
"""
|
|
230
|
+
Collects all official and dev Kapsel plugins and installs them concurrently across 4 threads.
|
|
231
|
+
"""
|
|
232
|
+
from kapsel.core.plugin.catalog import load_plugin_catalog_rich, DEFAULT_CATALOG
|
|
233
|
+
catalog = load_plugin_catalog_rich()
|
|
234
|
+
if not catalog:
|
|
235
|
+
catalog = {k: {"description": v} for k, v in DEFAULT_CATALOG.items()}
|
|
236
|
+
|
|
237
|
+
plugin_names: Set[str] = set()
|
|
238
|
+
for k in catalog.keys():
|
|
239
|
+
if k not in ("update", "all") and not k.startswith(("_", "$")):
|
|
240
|
+
plugin_names.add(k)
|
|
241
|
+
|
|
242
|
+
dev_plugins_dir = Path.cwd() / "plugins"
|
|
243
|
+
if is_dev and dev_plugins_dir.is_dir():
|
|
244
|
+
for item in dev_plugins_dir.iterdir():
|
|
245
|
+
if item.is_dir() and (item / "__init__.py").exists():
|
|
246
|
+
if item.name.lower() not in ("update", "all"):
|
|
247
|
+
plugin_names.add(item.name.lower())
|
|
248
|
+
|
|
249
|
+
ordered_plugins = sorted(plugin_names)
|
|
250
|
+
return _handle_add_batch_concurrent(ordered_plugins, con, is_dev=is_dev, max_workers=max_workers)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _select_plugins_interactive(con: Console, is_dev: bool = False) -> List[str]:
|
|
254
|
+
"""
|
|
255
|
+
Interactive multi-selection menu for selecting plugins to install.
|
|
256
|
+
Uses prompt_toolkit checkbox navigation when running in a TTY,
|
|
257
|
+
with a clean numeric selection fallback.
|
|
258
|
+
"""
|
|
259
|
+
from kapsel.core.plugin.catalog import load_plugin_catalog_rich, DEFAULT_CATALOG
|
|
260
|
+
catalog = load_plugin_catalog_rich()
|
|
261
|
+
if not catalog:
|
|
262
|
+
catalog = {k: {"description": v} for k, v in DEFAULT_CATALOG.items()}
|
|
263
|
+
|
|
264
|
+
plugin_names: Set[str] = set()
|
|
265
|
+
for k in catalog.keys():
|
|
266
|
+
if k not in ("update", "all") and not k.startswith(("_", "$")):
|
|
267
|
+
plugin_names.add(k)
|
|
268
|
+
|
|
269
|
+
dev_plugins_dir = Path.cwd() / "plugins"
|
|
270
|
+
if is_dev and dev_plugins_dir.is_dir():
|
|
271
|
+
for item in dev_plugins_dir.iterdir():
|
|
272
|
+
if item.is_dir() and (item / "__init__.py").exists():
|
|
273
|
+
if item.name.lower() not in ("update", "all"):
|
|
274
|
+
plugin_names.add(item.name.lower())
|
|
275
|
+
|
|
276
|
+
ordered_plugins = sorted(plugin_names)
|
|
277
|
+
if not ordered_plugins:
|
|
278
|
+
con.print("[yellow]No plugins found in catalog.[/]")
|
|
279
|
+
return []
|
|
280
|
+
|
|
281
|
+
cfg = load_config()
|
|
282
|
+
already_enabled = set(cfg.enabled_plugins)
|
|
283
|
+
|
|
284
|
+
con.print("\n[bold #00f0ff]🧩 Kapsel Plugin Multi-Selection Menu[/]")
|
|
285
|
+
con.print("[dim]Choose plugins to install and enable concurrently (4-Thread Worker Engine).[/]\n")
|
|
286
|
+
|
|
287
|
+
if not sys.stdin.isatty():
|
|
288
|
+
return _select_plugins_numeric_fallback(ordered_plugins, already_enabled, catalog, con)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
from prompt_toolkit.application import Application
|
|
292
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
293
|
+
from prompt_toolkit.layout.containers import HSplit, Window
|
|
294
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
295
|
+
from prompt_toolkit.layout.layout import Layout
|
|
296
|
+
from prompt_toolkit.styles import Style
|
|
297
|
+
|
|
298
|
+
cursor_idx = 0
|
|
299
|
+
selected_set: Set[str] = set()
|
|
300
|
+
confirmed = [False]
|
|
301
|
+
|
|
302
|
+
def get_menu_text():
|
|
303
|
+
lines = []
|
|
304
|
+
for i, name in enumerate(ordered_plugins):
|
|
305
|
+
is_cursor = (i == cursor_idx)
|
|
306
|
+
is_checked = name in selected_set
|
|
307
|
+
is_installed = name in already_enabled
|
|
308
|
+
|
|
309
|
+
prefix = " ❯ " if is_cursor else " "
|
|
310
|
+
checkbox = "[*] " if is_checked else "[ ] "
|
|
311
|
+
status_tag = " (already enabled)" if is_installed else ""
|
|
312
|
+
desc = catalog.get(name, {}).get("description", "")
|
|
313
|
+
if desc and len(desc) > 50:
|
|
314
|
+
desc = desc[:48] + "..."
|
|
315
|
+
desc_str = f" - {desc}" if desc else ""
|
|
316
|
+
|
|
317
|
+
line = f"{prefix}{checkbox}{name:<14}{status_tag:<18}{desc_str}\n"
|
|
318
|
+
|
|
319
|
+
if is_cursor:
|
|
320
|
+
lines.append(("class:selected", line))
|
|
321
|
+
elif is_checked:
|
|
322
|
+
lines.append(("class:checked", line))
|
|
323
|
+
else:
|
|
324
|
+
lines.append(("class:normal", line))
|
|
325
|
+
|
|
326
|
+
lines.append(("\nclass:help", " [↑/↓] Navigate | [Space] Toggle | [a] Toggle All | [Enter] Confirm & Install | [Esc/q] Cancel\n"))
|
|
327
|
+
return lines
|
|
328
|
+
|
|
329
|
+
kb = KeyBindings()
|
|
330
|
+
|
|
331
|
+
@kb.add("up")
|
|
332
|
+
def _up(event):
|
|
333
|
+
nonlocal cursor_idx
|
|
334
|
+
cursor_idx = (cursor_idx - 1) % len(ordered_plugins)
|
|
335
|
+
|
|
336
|
+
@kb.add("down")
|
|
337
|
+
def _down(event):
|
|
338
|
+
nonlocal cursor_idx
|
|
339
|
+
cursor_idx = (cursor_idx + 1) % len(ordered_plugins)
|
|
340
|
+
|
|
341
|
+
@kb.add("space")
|
|
342
|
+
def _toggle(event):
|
|
343
|
+
cur_name = ordered_plugins[cursor_idx]
|
|
344
|
+
if cur_name in selected_set:
|
|
345
|
+
selected_set.remove(cur_name)
|
|
346
|
+
else:
|
|
347
|
+
selected_set.add(cur_name)
|
|
348
|
+
|
|
349
|
+
@kb.add("a")
|
|
350
|
+
def _toggle_all(event):
|
|
351
|
+
if len(selected_set) == len(ordered_plugins):
|
|
352
|
+
selected_set.clear()
|
|
353
|
+
else:
|
|
354
|
+
selected_set.update(ordered_plugins)
|
|
355
|
+
|
|
356
|
+
@kb.add("enter")
|
|
357
|
+
def _confirm(event):
|
|
358
|
+
confirmed[0] = True
|
|
359
|
+
event.app.exit()
|
|
360
|
+
|
|
361
|
+
@kb.add("escape")
|
|
362
|
+
@kb.add("q")
|
|
363
|
+
def _cancel(event):
|
|
364
|
+
confirmed[0] = False
|
|
365
|
+
event.app.exit()
|
|
366
|
+
|
|
367
|
+
style = Style.from_dict({
|
|
368
|
+
"selected": "bold fg:#00f0ff bg:#1e293b",
|
|
369
|
+
"checked": "bold fg:#10b981",
|
|
370
|
+
"normal": "fg:#e2e8f0",
|
|
371
|
+
"help": "dim fg:#94a3b8",
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
layout = Layout(HSplit([Window(content=FormattedTextControl(get_menu_text))]))
|
|
375
|
+
app = Application(layout=layout, key_bindings=kb, style=style, full_screen=False)
|
|
376
|
+
app.run()
|
|
377
|
+
|
|
378
|
+
if confirmed[0]:
|
|
379
|
+
return [name for name in ordered_plugins if name in selected_set]
|
|
380
|
+
return []
|
|
381
|
+
|
|
382
|
+
except Exception:
|
|
383
|
+
return _select_plugins_numeric_fallback(ordered_plugins, already_enabled, catalog, con)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _select_plugins_numeric_fallback(
|
|
387
|
+
ordered_plugins: List[str],
|
|
388
|
+
already_enabled: Set[str],
|
|
389
|
+
catalog: dict,
|
|
390
|
+
con: Console,
|
|
391
|
+
) -> List[str]:
|
|
392
|
+
"""Fallback numbered list selection for non-interactive environments."""
|
|
393
|
+
for idx, name in enumerate(ordered_plugins, 1):
|
|
394
|
+
status = "[bold #10b981](enabled)[/]" if name in already_enabled else ""
|
|
395
|
+
desc = catalog.get(name, {}).get("description", "")
|
|
396
|
+
con.print(f" [bold cyan][{idx:2d}][/] [white]{name:<14}[/] {status} [dim]{desc[:50]}[/]")
|
|
397
|
+
|
|
398
|
+
con.print("")
|
|
399
|
+
try:
|
|
400
|
+
raw = input("Enter plugin numbers (comma-separated, e.g. 1, 3, 5), 'all', or 'q' to cancel: ").strip()
|
|
401
|
+
if raw.lower() in ("q", "quit", "exit", ""):
|
|
402
|
+
return []
|
|
403
|
+
if raw.lower() == "all":
|
|
404
|
+
return ordered_plugins
|
|
405
|
+
|
|
406
|
+
selected = []
|
|
407
|
+
for part in raw.replace(" ", ",").split(","):
|
|
408
|
+
part = part.strip()
|
|
409
|
+
if part.isdigit():
|
|
410
|
+
num = int(part)
|
|
411
|
+
if 1 <= num <= len(ordered_plugins):
|
|
412
|
+
selected.append(ordered_plugins[num - 1])
|
|
413
|
+
return selected
|
|
414
|
+
except Exception:
|
|
415
|
+
return []
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def handle_add_command(args: List[str], console: Optional[Console] = None) -> int:
|
|
419
|
+
"""
|
|
420
|
+
Handles 'kapsel add <plugin_name...>' system command.
|
|
421
|
+
Supports:
|
|
422
|
+
- Interactive multi-selection: 'kapsel add' or 'kapsel add -i'
|
|
423
|
+
- Multiple CLI arguments: 'kapsel add portal backup preview'
|
|
424
|
+
- Comma-separated plugins: 'kapsel add portal,backup,preview'
|
|
425
|
+
- 4-Thread concurrent execution: 'kapsel add all' or multi-plugins
|
|
426
|
+
"""
|
|
427
|
+
ensure_utf8_io()
|
|
428
|
+
con = console or Console(legacy_windows=False)
|
|
429
|
+
cfg = load_config()
|
|
430
|
+
is_dev = getattr(cfg, "is_dev", False)
|
|
431
|
+
|
|
432
|
+
# 1. Interactive multi-selection mode if no args or explicit interactive flag
|
|
433
|
+
interactive_flags = {"-i", "--interactive", "--select", "select"}
|
|
434
|
+
if not args or any(a in interactive_flags for a in args):
|
|
435
|
+
chosen = _select_plugins_interactive(con, is_dev=is_dev)
|
|
436
|
+
if not chosen:
|
|
437
|
+
con.print("[dim]No plugins selected. Operation cancelled.[/]\n")
|
|
438
|
+
return 0
|
|
439
|
+
if len(chosen) == 1:
|
|
440
|
+
success, msg = _install_single_plugin(chosen[0], con, is_dev=is_dev, is_batch=False)
|
|
441
|
+
if success:
|
|
442
|
+
con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
|
|
443
|
+
return 0
|
|
444
|
+
return 1
|
|
445
|
+
return _handle_add_batch_concurrent(chosen, con, is_dev=is_dev, max_workers=4)
|
|
446
|
+
|
|
447
|
+
# 2. Parse target plugin names from CLI arguments (supporting spaces and commas)
|
|
448
|
+
targets: List[str] = []
|
|
449
|
+
for arg in args:
|
|
450
|
+
for item in arg.split(","):
|
|
451
|
+
item_clean = item.strip()
|
|
452
|
+
if item_clean and item_clean not in interactive_flags:
|
|
453
|
+
targets.append(item_clean)
|
|
454
|
+
|
|
455
|
+
if not targets:
|
|
456
|
+
con.print("[bold #f43f5e]Error:[/] Please specify plugin name(s) to add.")
|
|
457
|
+
con.print("[dim]Usage: kapsel add <name...> | all | update | -i (interactive multi-select)[/]\n")
|
|
458
|
+
return 1
|
|
459
|
+
|
|
460
|
+
# 3. Handle 'kapsel add update'
|
|
461
|
+
if len(targets) == 1 and targets[0].lower() == "update":
|
|
462
|
+
from kapsel.core.plugin.catalog import update_plugin_catalog, render_catalog_table
|
|
463
|
+
con.print("[bold #00f0ff]🔄 Scanning repository plugins and updating catalog dictionary...[/]")
|
|
464
|
+
updated_catalog = update_plugin_catalog(con)
|
|
465
|
+
render_catalog_table(updated_catalog, con)
|
|
466
|
+
con.print("[bold #10b981]✔ Plugin catalog and subcommands completion dictionary successfully updated![/]\n")
|
|
467
|
+
return 0
|
|
468
|
+
|
|
469
|
+
# 4. Handle 'kapsel add all'
|
|
470
|
+
if any(t.lower() == "all" for t in targets):
|
|
471
|
+
return _handle_add_all(con, is_dev=is_dev, max_workers=4)
|
|
472
|
+
|
|
473
|
+
# 5. Handle single plugin installation
|
|
474
|
+
if len(targets) == 1:
|
|
475
|
+
success, msg = _install_single_plugin(targets[0], con, is_dev=is_dev, is_batch=False)
|
|
476
|
+
if success:
|
|
477
|
+
con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
|
|
478
|
+
return 0
|
|
479
|
+
else:
|
|
480
|
+
return 1
|
|
481
|
+
|
|
482
|
+
# 6. Handle multiple plugins concurrent installation (4 workers)
|
|
483
|
+
return _handle_add_batch_concurrent(targets, con, is_dev=is_dev, max_workers=4)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kapsel Restart Built-in Command.
|
|
3
|
+
Terminates any background tasks, cleanly tears down the active shell state,
|
|
4
|
+
and re-launches the Kapsel interactive shell session.
|
|
5
|
+
All comments and docstrings are in English.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from kapsel.core.block.registry import get_block_registry
|
|
17
|
+
from kapsel.i18n import _
|
|
18
|
+
from kapsel.storage.logger import logger
|
|
19
|
+
from kapsel.ui.banner import ensure_utf8_io
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def handle_restart(console: Optional[Console] = None) -> int:
|
|
23
|
+
"""
|
|
24
|
+
Tears down current Kapsel shell session and immediately spawns a fresh Kapsel session.
|
|
25
|
+
"""
|
|
26
|
+
ensure_utf8_io()
|
|
27
|
+
con = console or Console(legacy_windows=False)
|
|
28
|
+
|
|
29
|
+
# 1. Cleanly terminate any active background block tasks
|
|
30
|
+
try:
|
|
31
|
+
cleaned = get_block_registry().terminate_all_running()
|
|
32
|
+
if cleaned > 0:
|
|
33
|
+
con.print(f"[dim]⚡ Cleaned up {cleaned} background block task(s).[/]")
|
|
34
|
+
except Exception as e:
|
|
35
|
+
logger.debug(f"Error cleaning background tasks on restart: {e}")
|
|
36
|
+
|
|
37
|
+
# 2. Reset active shell marker
|
|
38
|
+
os.environ.pop("KAPSEL_ACTIVE", None)
|
|
39
|
+
|
|
40
|
+
con.print("[bold #00f0ff]🔄 Restarting Kapsel interactive shell...[/]\n")
|
|
41
|
+
|
|
42
|
+
# 3. Determine restart command args
|
|
43
|
+
raw_argv = sys.argv
|
|
44
|
+
clean_args = [a for a in raw_argv[1:] if a != "restart"]
|
|
45
|
+
|
|
46
|
+
target = raw_argv[0]
|
|
47
|
+
if (
|
|
48
|
+
target.lower().endswith((".exe", "kapsel"))
|
|
49
|
+
and Path(target).is_file()
|
|
50
|
+
):
|
|
51
|
+
cmd = [target] + clean_args
|
|
52
|
+
else:
|
|
53
|
+
cmd = [sys.executable, "-m", "kapsel"] + clean_args
|
|
54
|
+
|
|
55
|
+
# 4. Re-execute process
|
|
56
|
+
try:
|
|
57
|
+
if os.name != "nt":
|
|
58
|
+
# On POSIX, replace the current process image
|
|
59
|
+
os.execv(cmd[0], cmd)
|
|
60
|
+
else:
|
|
61
|
+
# On Windows, launch child and wait synchronously to maintain console ownership
|
|
62
|
+
code = subprocess.call(cmd)
|
|
63
|
+
sys.exit(code)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Failed to restart Kapsel: {e}")
|
|
66
|
+
con.print(f"[bold red]Failed to restart Kapsel:[/] {e}")
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def handle_restart_command(args: Optional[List[str]] = None, console: Optional[Console] = None) -> int:
|
|
73
|
+
"""CLI handler for 'kapsel restart'."""
|
|
74
|
+
return handle_restart(console=console)
|
|
@@ -65,11 +65,15 @@ def handle_status(args: Optional[List[str]] = None, console: Optional[Console] =
|
|
|
65
65
|
f"[bold #10b981]{branch}[/]" if branch else f"[dim]{none_str}[/]",
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
+
from kapsel.core.update_check import get_pypi_upgrade_notice
|
|
69
|
+
up_ver = get_pypi_upgrade_notice()
|
|
70
|
+
up_badge = f" [bold #f59e0b]🚀 upgrade (v{up_ver})[/]" if up_ver else ""
|
|
71
|
+
|
|
68
72
|
grid.add_row(
|
|
69
73
|
f"📂 {_('Working Dir:')}",
|
|
70
74
|
f"[dim]{cwd_fmt}[/]",
|
|
71
75
|
f"💊 {_('Kapsel Version:')}",
|
|
72
|
-
f"[bold #00f0ff]v{__version__}[/] (Python {sys.version.split()[0]})",
|
|
76
|
+
f"[bold #00f0ff]v{__version__}[/]{up_badge} (Python {sys.version.split()[0]})",
|
|
73
77
|
)
|
|
74
78
|
|
|
75
79
|
theme_name = cfg.theme.get("name", "cyber_dark") if isinstance(cfg.theme, dict) else str(cfg.theme)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kapsel Core Declarative Tool Dependency Registry & Installer Package.
|
|
3
|
+
Provides centralized access to aggregated tools across plugins and deterministic installation.
|
|
4
|
+
|
|
5
|
+
All comments and docstrings are in English.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .registry import (
|
|
9
|
+
ToolRegistry,
|
|
10
|
+
get_registry,
|
|
11
|
+
get_tool,
|
|
12
|
+
get_all_tools,
|
|
13
|
+
get_plugin_tools,
|
|
14
|
+
)
|
|
15
|
+
from .installer import (
|
|
16
|
+
ToolInstaller,
|
|
17
|
+
get_kapsel_bin_dir,
|
|
18
|
+
is_tool_installed,
|
|
19
|
+
resolve_tool_executable,
|
|
20
|
+
install_tool,
|
|
21
|
+
install_plugin_tools,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ToolRegistry",
|
|
26
|
+
"get_registry",
|
|
27
|
+
"get_tool",
|
|
28
|
+
"get_all_tools",
|
|
29
|
+
"get_plugin_tools",
|
|
30
|
+
"ToolInstaller",
|
|
31
|
+
"get_kapsel_bin_dir",
|
|
32
|
+
"is_tool_installed",
|
|
33
|
+
"resolve_tool_executable",
|
|
34
|
+
"install_tool",
|
|
35
|
+
"install_plugin_tools",
|
|
36
|
+
]
|