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.
Files changed (125) hide show
  1. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/PKG-INFO +1 -1
  2. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/__init__.py +1 -1
  3. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/cli.py +9 -0
  4. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/__init__.py +10 -0
  5. kapsel_cli-0.5.7/kapsel/completion/kps/builtins/add.py +483 -0
  6. kapsel_cli-0.5.7/kapsel/completion/kps/builtins/restart.py +74 -0
  7. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/status.py +5 -1
  8. kapsel_cli-0.5.7/kapsel/core/tools/__init__.py +36 -0
  9. kapsel_cli-0.5.7/kapsel/core/tools/installer.py +374 -0
  10. kapsel_cli-0.5.7/kapsel/core/tools/registry.py +219 -0
  11. kapsel_cli-0.5.7/kapsel/core/update_check.py +140 -0
  12. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/banner.py +7 -0
  13. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/PKG-INFO +1 -1
  14. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/SOURCES.txt +10 -0
  15. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/pyproject.toml +1 -1
  16. kapsel_cli-0.5.7/tests/test_add_concurrent.py +81 -0
  17. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_backup.py +1 -1
  18. kapsel_cli-0.5.7/tests/test_plugin_help.py +204 -0
  19. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_install.py +73 -0
  20. kapsel_cli-0.5.7/tests/test_plugin_mend.py +154 -0
  21. kapsel_cli-0.5.7/tests/test_restart_and_pypi.py +85 -0
  22. kapsel_cli-0.5.7/tests/test_tools_registry.py +87 -0
  23. kapsel_cli-0.5.4/kapsel/completion/kps/builtins/add.py +0 -218
  24. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/README.md +0 -0
  25. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/__main__.py +0 -0
  26. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/__init__.py +0 -0
  27. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/carapace_engine.py +0 -0
  28. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/carapace_installer.py +0 -0
  29. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/completer.py +0 -0
  30. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/__init__.py +0 -0
  31. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/completion.py +0 -0
  32. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/config.py +0 -0
  33. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/datadir.py +0 -0
  34. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/dir_nav.py +0 -0
  35. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/help.py +0 -0
  36. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/language.py +0 -0
  37. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/plugin_switch.py +0 -0
  38. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/search.py +0 -0
  39. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/toggle.py +0 -0
  40. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/builtins/upgrade.py +0 -0
  41. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/dispatcher.py +0 -0
  42. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/kps/registry.py +0 -0
  43. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/completion/spec_manager.py +0 -0
  44. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/__init__.py +0 -0
  45. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/block/__init__.py +0 -0
  46. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/block/model.py +0 -0
  47. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/block/registry.py +0 -0
  48. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/block/runner.py +0 -0
  49. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/detector.py +0 -0
  50. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/engine.py +0 -0
  51. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/executor.py +0 -0
  52. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/i18n.py +0 -0
  53. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/__init__.py +0 -0
  54. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/base.py +0 -0
  55. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/catalog.py +0 -0
  56. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/context.py +0 -0
  57. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/fetcher.py +0 -0
  58. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/hooks.py +0 -0
  59. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/core/plugin/manager.py +0 -0
  60. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/i18n.py +0 -0
  61. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
  62. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
  63. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/de/help.yaml +0 -0
  64. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/de/tips.yaml +0 -0
  65. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/en/help.yaml +0 -0
  66. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/en/tips.yaml +0 -0
  67. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
  68. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
  69. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/es/help.yaml +0 -0
  70. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/es/tips.yaml +0 -0
  71. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
  72. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
  73. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/fr/help.yaml +0 -0
  74. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/fr/tips.yaml +0 -0
  75. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
  76. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
  77. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ja/help.yaml +0 -0
  78. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ja/tips.yaml +0 -0
  79. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/kapsel.pot +0 -0
  80. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
  81. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
  82. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ru/help.yaml +0 -0
  83. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/ru/tips.yaml +0 -0
  84. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
  85. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
  86. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/zh_CN/help.yaml +0 -0
  87. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/locales/zh_CN/tips.yaml +0 -0
  88. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/storage/__init__.py +0 -0
  89. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/storage/config.py +0 -0
  90. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/storage/history.py +0 -0
  91. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/storage/logger.py +0 -0
  92. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/storage/migrate.py +0 -0
  93. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/__init__.py +0 -0
  94. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/card.py +0 -0
  95. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/prompt.py +0 -0
  96. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/theme.py +0 -0
  97. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/tips.py +0 -0
  98. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel/ui/viewport.py +0 -0
  99. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/dependency_links.txt +0 -0
  100. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/entry_points.txt +0 -0
  101. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/requires.txt +0 -0
  102. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/kapsel_cli.egg-info/top_level.txt +0 -0
  103. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/setup.cfg +0 -0
  104. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_block_system.py +0 -0
  105. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_carapace_installer.py +0 -0
  106. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_config.py +0 -0
  107. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_dir_nav.py +0 -0
  108. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_history.py +0 -0
  109. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_i18n.py +0 -0
  110. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_ai.py +0 -0
  111. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_alias.py +0 -0
  112. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_autopilot.py +0 -0
  113. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_commit.py +0 -0
  114. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_fetcher.py +0 -0
  115. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_init.py +0 -0
  116. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_lang.py +0 -0
  117. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_portal.py +0 -0
  118. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_preview.py +0 -0
  119. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_queue.py +0 -0
  120. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_template.py +0 -0
  121. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_plugin_trans.py +0 -0
  122. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_search_and_completions.py +0 -0
  123. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_spec_manager.py +0 -0
  124. {kapsel_cli-0.5.4 → kapsel_cli-0.5.7}/tests/test_toggle.py +0 -0
  125. {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.4
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
@@ -3,4 +3,4 @@ Kapsel: Cross-platform adaptive smart terminal capsule.
3
3
  Wrap complexity, expose simplicity.
4
4
  """
5
5
 
6
- __version__ = "0.5.4"
6
+ __version__ = "0.5.7"
@@ -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
+ ]