kapsel-cli 0.3.6__tar.gz → 0.3.8__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 (104) hide show
  1. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/PKG-INFO +1 -1
  2. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/__init__.py +1 -1
  3. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/add.py +132 -51
  4. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/upgrade.py +11 -9
  5. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/spec_manager.py +5 -3
  6. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/PKG-INFO +1 -1
  7. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/pyproject.toml +1 -1
  8. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_fetcher.py +10 -0
  9. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/README.md +0 -0
  10. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/__main__.py +0 -0
  11. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/cli.py +0 -0
  12. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/__init__.py +0 -0
  13. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/carapace_engine.py +0 -0
  14. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/carapace_installer.py +0 -0
  15. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/completer.py +0 -0
  16. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/__init__.py +0 -0
  17. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/__init__.py +0 -0
  18. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/completion.py +0 -0
  19. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/config.py +0 -0
  20. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/datadir.py +0 -0
  21. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/help.py +0 -0
  22. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/language.py +0 -0
  23. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/plugin_switch.py +0 -0
  24. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/search.py +0 -0
  25. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/status.py +0 -0
  26. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/toggle.py +0 -0
  27. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/dispatcher.py +0 -0
  28. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/registry.py +0 -0
  29. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/__init__.py +0 -0
  30. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/block/__init__.py +0 -0
  31. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/block/model.py +0 -0
  32. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/block/registry.py +0 -0
  33. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/block/runner.py +0 -0
  34. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/detector.py +0 -0
  35. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/engine.py +0 -0
  36. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/executor.py +0 -0
  37. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/i18n.py +0 -0
  38. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/__init__.py +0 -0
  39. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/base.py +0 -0
  40. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/catalog.py +0 -0
  41. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/context.py +0 -0
  42. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/fetcher.py +0 -0
  43. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/hooks.py +0 -0
  44. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/manager.py +0 -0
  45. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/i18n.py +0 -0
  46. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
  47. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
  48. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/de/help.yaml +0 -0
  49. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/de/tips.yaml +0 -0
  50. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/en/help.yaml +0 -0
  51. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/en/tips.yaml +0 -0
  52. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
  53. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
  54. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/es/help.yaml +0 -0
  55. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/es/tips.yaml +0 -0
  56. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
  57. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
  58. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/fr/help.yaml +0 -0
  59. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/fr/tips.yaml +0 -0
  60. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
  61. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
  62. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ja/help.yaml +0 -0
  63. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ja/tips.yaml +0 -0
  64. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/kapsel.pot +0 -0
  65. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
  66. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
  67. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ru/help.yaml +0 -0
  68. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ru/tips.yaml +0 -0
  69. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
  70. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
  71. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/zh_CN/help.yaml +0 -0
  72. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/zh_CN/tips.yaml +0 -0
  73. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/storage/__init__.py +0 -0
  74. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/storage/config.py +0 -0
  75. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/storage/history.py +0 -0
  76. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/storage/logger.py +0 -0
  77. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/storage/migrate.py +0 -0
  78. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/__init__.py +0 -0
  79. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/banner.py +0 -0
  80. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/card.py +0 -0
  81. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/prompt.py +0 -0
  82. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/theme.py +0 -0
  83. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/tips.py +0 -0
  84. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/viewport.py +0 -0
  85. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/SOURCES.txt +0 -0
  86. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/dependency_links.txt +0 -0
  87. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/entry_points.txt +0 -0
  88. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/requires.txt +0 -0
  89. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/top_level.txt +0 -0
  90. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/setup.cfg +0 -0
  91. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_block_system.py +0 -0
  92. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_carapace_installer.py +0 -0
  93. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_config.py +0 -0
  94. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_history.py +0 -0
  95. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_i18n.py +0 -0
  96. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_ai.py +0 -0
  97. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_alias.py +0 -0
  98. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_autopilot.py +0 -0
  99. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_init.py +0 -0
  100. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_install.py +0 -0
  101. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_portal.py +0 -0
  102. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_search_and_completions.py +0 -0
  103. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_spec_manager.py +0 -0
  104. {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/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.6
3
+ Version: 0.3.8
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.3.6"
6
+ __version__ = "0.3.8"
@@ -9,93 +9,72 @@ import importlib.util
9
9
  from pathlib import Path
10
10
  import shutil
11
11
  import sys
12
- from typing import List, Optional
12
+ from typing import List, Optional, Tuple
13
13
  from rich.console import Console
14
14
  from rich.panel import Panel
15
+ from rich.table import Table
15
16
 
16
17
  from kapsel.storage.config import get_kapsel_dir, load_config, update_config_value
17
18
  from kapsel.ui.banner import ensure_utf8_io
18
19
 
19
20
 
20
- def handle_add_command(args: List[str], console: Optional[Console] = None) -> int:
21
+ def _install_single_plugin(
22
+ target_raw: str,
23
+ con: Console,
24
+ is_dev: bool = False,
25
+ is_batch: bool = False,
26
+ ) -> Tuple[bool, str]:
21
27
  """
22
- Handles 'kapsel add <plugin_name>' system command.
23
- Enables local plugins, triggers their standalone install.py, and registers them into config.yaml.
28
+ Installs a single plugin by name or path:
29
+ 1. Resolves source directory (dev local, user global, or remote fetch).
30
+ 2. Copies to user global plugins directory.
31
+ 3. Adds to config.yaml enabled plugins list.
32
+ 4. Triggers standalone install.py if present.
33
+ Returns (success: bool, message: str).
24
34
  """
25
- ensure_utf8_io()
26
- con = console or Console(legacy_windows=False)
27
-
28
- if not args:
29
- con.print("[bold #f43f5e]Error:[/] Please specify a plugin name to add.")
30
- con.print("[dim]Usage: kapsel add <plugin_name> (e.g. kapsel add install)[/]\n")
31
- return 1
32
-
33
- target_raw = args[0].strip()
34
-
35
- # Handle 'kapsel add update' to scan and refresh catalog subcommands dictionary
36
- if target_raw.lower() == "update":
37
- from kapsel.core.plugin.catalog import update_plugin_catalog, render_catalog_table
38
- con.print("[bold #00f0ff]🔄 Scanning repository plugins and updating catalog dictionary...[/]")
39
- updated_catalog = update_plugin_catalog(con)
40
- render_catalog_table(updated_catalog, con)
41
- con.print("[bold #10b981]✔ Plugin catalog and subcommands completion dictionary successfully updated![/]\n")
42
- return 0
43
-
44
- cfg = load_config()
45
- is_dev = getattr(cfg, "is_dev", False)
46
-
47
- source_path = Path(target_raw).expanduser()
48
-
49
35
  global_plugins_dir = get_kapsel_dir() / "plugins"
50
36
  global_plugins_dir.mkdir(parents=True, exist_ok=True)
51
37
 
52
- # Determine plugin ID and source:
53
- # In release mode, bare names like 'install' will never be hijacked by local folders.
54
- # Only treat as a local directory if explicit path syntax is used (./, ../, /, \) or in dev mode.
38
+ source_path = Path(target_raw).expanduser()
55
39
  has_path_qualifier = any(sep in target_raw for sep in ("/", "\\")) or target_raw.startswith(".")
56
40
 
57
41
  if (has_path_qualifier or is_dev) and source_path.exists() and source_path.is_dir():
58
- # User specified an explicit directory path (or in dev mode where local directory takes priority)
59
42
  target_name = source_path.name.lower()
60
43
  dest_plugin_dir = global_plugins_dir / target_name
61
44
 
62
- # Verify source has __init__.py
63
45
  if not (source_path / "__init__.py").exists():
64
- con.print(f"[bold #f43f5e]Error:[/] Source directory '{source_path}' does not contain '__init__.py'.")
65
- return 1
46
+ err = f"Source directory '{source_path}' does not contain '__init__.py'."
47
+ con.print(f"[bold #f43f5e]Error:[/] {err}")
48
+ return False, err
66
49
 
67
- # Install / sync to user global plugin directory
68
50
  shutil.copytree(source_path, dest_plugin_dir, dirs_exist_ok=True)
69
51
  plugin_dir = dest_plugin_dir
70
-
71
52
  else:
72
- # User specified a plugin name (e.g. kapsel add install)
73
53
  target_name = target_raw.lower()
74
54
  dest_plugin_dir = global_plugins_dir / target_name
75
55
  dev_plugin_dir = Path.cwd() / "plugins" / target_name
76
56
 
77
57
  if is_dev and dev_plugin_dir.exists() and dev_plugin_dir.is_dir() and (dev_plugin_dir / "__init__.py").exists():
78
- # If present in local dev directory (dev mode only), sync to global user directory
79
58
  shutil.copytree(dev_plugin_dir, dest_plugin_dir, dirs_exist_ok=True)
80
59
  plugin_dir = dest_plugin_dir
81
60
  elif dest_plugin_dir.exists() and dest_plugin_dir.is_dir():
82
61
  plugin_dir = dest_plugin_dir
83
62
  else:
84
- # 3. Not found locally: Automatically fetch from official remote repository
85
63
  from kapsel.core.plugin.fetcher import fetch_plugin_from_remote
86
-
87
64
  if fetch_plugin_from_remote(target_name, dest_plugin_dir, con):
88
65
  plugin_dir = dest_plugin_dir
89
66
  else:
90
- con.print(f"[bold #f43f5e]Error:[/] Plugin '[white]{target_name}[/]' could not be installed.")
91
- con.print(f"[dim]Expected location:[/] {dest_plugin_dir}")
92
- con.print("[dim]To install a custom plugin, specify its local path or place it into your global plugins directory.[/]\n")
93
- return 1
67
+ err = f"Plugin '{target_name}' could not be installed."
68
+ con.print(f"[bold #f43f5e]Error:[/] {err}")
69
+ if not is_batch:
70
+ con.print(f"[dim]Expected location:[/] {dest_plugin_dir}")
71
+ con.print("[dim]To install a custom plugin, specify its local path or place it into your global plugins directory.[/]\n")
72
+ return False, err
94
73
 
95
- # Verify that the final plugin directory contains __init__.py
96
74
  if not (plugin_dir / "__init__.py").exists():
97
- con.print(f"[bold #f43f5e]Error:[/] Plugin directory '{plugin_dir}' does not contain a valid '__init__.py'.")
98
- return 1
75
+ err = f"Plugin directory '{plugin_dir}' does not contain a valid '__init__.py'."
76
+ con.print(f"[bold #f43f5e]Error:[/] {err}")
77
+ return False, err
99
78
 
100
79
  # Add to config.yaml enabled plugins list
101
80
  cfg = load_config()
@@ -105,7 +84,6 @@ def handle_add_command(args: List[str], console: Optional[Console] = None) -> in
105
84
  update_config_value("plugins", "enabled", new_enabled)
106
85
 
107
86
  # Decoupled plugin dependency installation:
108
- # Trigger standalone install.py if provided by the plugin
109
87
  install_script = plugin_dir / "install.py"
110
88
  if install_script.exists():
111
89
  try:
@@ -132,6 +110,109 @@ def handle_add_command(args: List[str], console: Optional[Console] = None) -> in
132
110
 
133
111
  msg = f"[bold #10b981]✔ Plugin '[#00f0ff]{target_name}[/]' (v{ver}) successfully added and enabled![/]\n\n"
134
112
  msg += f"[dim]Location: {plugin_dir}[/]"
113
+ return True, msg
114
+
115
+
116
+ def _handle_add_all(con: Console, is_dev: bool = False) -> int:
117
+ """
118
+ Sequentially installs and runs installation scripts for all official Kapsel plugins.
119
+ """
120
+ from kapsel.core.plugin.catalog import load_plugin_catalog_rich, DEFAULT_CATALOG
121
+ catalog = load_plugin_catalog_rich()
122
+ if not catalog:
123
+ catalog = {k: {"description": v} for k, v in DEFAULT_CATALOG.items()}
124
+
125
+ plugin_names = set()
126
+ for k in catalog.keys():
127
+ if k not in ("update", "all") and not k.startswith(("_", "$")):
128
+ plugin_names.add(k)
129
+
130
+ dev_plugins_dir = Path.cwd() / "plugins"
131
+ if is_dev and dev_plugins_dir.is_dir():
132
+ for item in dev_plugins_dir.iterdir():
133
+ if item.is_dir() and (item / "__init__.py").exists():
134
+ if item.name.lower() not in ("update", "all"):
135
+ plugin_names.add(item.name.lower())
136
+
137
+ ordered_plugins = sorted(plugin_names)
138
+ total = len(ordered_plugins)
139
+
140
+ con.print(f"\n[bold #00f0ff]============================================================[/]")
141
+ con.print(f"[bold #00f0ff] 📦 Kapsel Batch Plugin Installer: {total} Plugins Found [/]")
142
+ con.print(f"[bold #00f0ff]============================================================[/]\n")
143
+
144
+ succeeded: List[str] = []
145
+ failed: List[str] = []
146
+
147
+ for idx, name in enumerate(ordered_plugins, 1):
148
+ con.print(f"[bold #00f0ff]━━━ [{idx}/{total}] Adding & Installing Plugin: [white]{name}[/] ━━━[/]")
149
+ try:
150
+ ok, msg = _install_single_plugin(name, con, is_dev=is_dev, is_batch=True)
151
+ if ok:
152
+ succeeded.append(name)
153
+ con.print(f" [bold #10b981]✔ Added & configured successfully[/]\n")
154
+ else:
155
+ failed.append(name)
156
+ con.print(f" [bold #f43f5e]✘ Installation failed[/]\n")
157
+ except Exception as e:
158
+ failed.append(name)
159
+ con.print(f" [bold #f43f5e]✘ Unexpected error during '{name}' install:[/] {e}\n")
160
+
161
+ # Summary table
162
+ table = Table(title="[bold #00f0ff]📊 Batch Installation Summary[/]", border_style="#0891b2")
163
+ table.add_column("Plugin", style="#00f0ff")
164
+ table.add_column("Status", style="white")
165
+
166
+ for name in ordered_plugins:
167
+ if name in succeeded:
168
+ table.add_row(name, "[bold #10b981]✔ Installed & Enabled[/]")
169
+ else:
170
+ table.add_row(name, "[bold #f43f5e]✘ Failed[/]")
171
+
172
+ con.print(table)
173
+ summary_color = "#10b981" if not failed else "#f59e0b"
174
+ con.print(f"\n[{summary_color}]Batch Completed:[/] [white]{len(succeeded)} succeeded[/], "
175
+ f"[{'#f43f5e' if failed else 'white'}]{len(failed)} failed[/].\n")
176
+ return 0 if succeeded else 1
177
+
178
+
179
+ def handle_add_command(args: List[str], console: Optional[Console] = None) -> int:
180
+ """
181
+ Handles 'kapsel add <plugin_name>' system command.
182
+ Enables local plugins, triggers their standalone install.py, and registers them into config.yaml.
183
+ Supports 'kapsel add all' to sequentially install all available plugins.
184
+ """
185
+ ensure_utf8_io()
186
+ con = console or Console(legacy_windows=False)
187
+
188
+ if not args:
189
+ con.print("[bold #f43f5e]Error:[/] Please specify a plugin name to add.")
190
+ con.print("[dim]Usage: kapsel add <plugin_name> | all | update (e.g. kapsel add all)[/]\n")
191
+ return 1
192
+
193
+ target_raw = args[0].strip()
194
+
195
+ # Handle 'kapsel add update' to scan and refresh catalog subcommands dictionary
196
+ if target_raw.lower() == "update":
197
+ from kapsel.core.plugin.catalog import update_plugin_catalog, render_catalog_table
198
+ con.print("[bold #00f0ff]🔄 Scanning repository plugins and updating catalog dictionary...[/]")
199
+ updated_catalog = update_plugin_catalog(con)
200
+ render_catalog_table(updated_catalog, con)
201
+ con.print("[bold #10b981]✔ Plugin catalog and subcommands completion dictionary successfully updated![/]\n")
202
+ return 0
203
+
204
+ cfg = load_config()
205
+ is_dev = getattr(cfg, "is_dev", False)
206
+
207
+ # Handle 'kapsel add all' to sequentially install all available plugins
208
+ if target_raw.lower() == "all":
209
+ return _handle_add_all(con, is_dev=is_dev)
210
+
211
+ # Handle single plugin installation
212
+ success, msg = _install_single_plugin(target_raw, con, is_dev=is_dev, is_batch=False)
213
+ if success:
214
+ con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
215
+ return 0
216
+ else:
217
+ return 1
135
218
 
136
- con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
137
- return 0
@@ -46,9 +46,11 @@ OFFICIAL_PLUGINS_CATALOG_URL = "https://raw.githubusercontent.com/MrEiu/plugins/
46
46
  OFFICIAL_PLUGINS_CATALOG_MIRROR_URL = "https://ghproxy.net/https://raw.githubusercontent.com/MrEiu/plugins/master/catalog.json"
47
47
 
48
48
 
49
- def _parse_semver(ver_str: str) -> Tuple[int, ...]:
49
+ def _parse_semver(ver_str: Optional[str]) -> Tuple[int, ...]:
50
50
  """Parses a semver version string into an integer tuple for accurate comparison."""
51
- clean = re.sub(r"^[vV]", "", ver_str.strip())
51
+ if not ver_str:
52
+ return (0, 0, 0)
53
+ clean = re.sub(r"^[vV]", "", str(ver_str).strip())
52
54
  parts = []
53
55
  for part in clean.split("."):
54
56
  num_m = re.match(r"^(\d+)", part)
@@ -128,11 +130,11 @@ def check_kapsel_core_update() -> Dict[str, Any]:
128
130
  # 1. Try GitHub Releases API first (provides rich release notes)
129
131
  gh_data = _fetch_url_json([KAPSEL_GITHUB_RELEASE_API, KAPSEL_GITHUB_RELEASE_MIRROR_API])
130
132
  if gh_data and "tag_name" in gh_data:
131
- latest_tag = gh_data["tag_name"]
132
- latest_ver = re.sub(r"^[vV]", "", latest_tag)
133
+ latest_tag = gh_data.get("tag_name") or ""
134
+ latest_ver = re.sub(r"^[vV]", "", str(latest_tag).strip())
133
135
  res["latest"] = latest_ver
134
- res["notes"] = gh_data.get("body", "").strip()
135
- res["published_at"] = gh_data.get("published_at", "")[:10]
136
+ res["notes"] = (gh_data.get("body") or "").strip()
137
+ res["published_at"] = (gh_data.get("published_at") or "")[:10]
136
138
  if _parse_semver(latest_ver) > _parse_semver(CURRENT_KAPSEL_VERSION):
137
139
  res["has_update"] = True
138
140
  return res
@@ -140,11 +142,11 @@ def check_kapsel_core_update() -> Dict[str, Any]:
140
142
  # 2. Fallback to PyPI JSON API
141
143
  pypi_data = _fetch_url_json([KAPSEL_PYPI_JSON_API, KAPSEL_PYPI_MIRROR_JSON_API])
142
144
  if pypi_data and "info" in pypi_data:
143
- info = pypi_data["info"]
144
- latest_ver = info.get("version", CURRENT_KAPSEL_VERSION)
145
+ info = pypi_data.get("info") or {}
146
+ latest_ver = info.get("version") or CURRENT_KAPSEL_VERSION
145
147
  res["latest"] = latest_ver
146
148
  if not res["notes"]:
147
- res["notes"] = info.get("summary", "")
149
+ res["notes"] = (info.get("summary") or "").strip()
148
150
  if _parse_semver(latest_ver) > _parse_semver(CURRENT_KAPSEL_VERSION):
149
151
  res["has_update"] = True
150
152
  return res
@@ -79,6 +79,7 @@ CORE_SYSTEM_COMMANDS: List[Dict[str, Any]] = [
79
79
  "name": "add",
80
80
  "description": "Enable and register a plugin into Kapsel environment",
81
81
  "commands": [
82
+ {"name": "all", "description": "Install and enable all official Kapsel plugins sequentially"},
82
83
  {"name": "update", "description": "Scan plugins and update completion dictionary"},
83
84
  ],
84
85
  },
@@ -420,10 +421,11 @@ class CarapaceSpecManager:
420
421
  from kapsel.core.plugin.catalog import load_plugin_catalog
421
422
  catalog = load_plugin_catalog()
422
423
  add_subcmds: List[Dict[str, Any]] = [
423
- {"name": "update", "description": "Scan plugins and update completion dictionary"}
424
+ {"name": "all", "description": "Install and enable all official Kapsel plugins sequentially"},
425
+ {"name": "update", "description": "Scan plugins and update completion dictionary"},
424
426
  ]
425
427
  for p_name, p_desc in sorted(catalog.items()):
426
- if p_name != "update":
428
+ if p_name not in ("update", "all"):
427
429
  add_subcmds.append({"name": p_name, "description": p_desc})
428
430
  for d_name, d_info in sorted(discovered_specs.items()):
429
431
  if not any(c["name"] == d_name for c in add_subcmds):
@@ -431,7 +433,7 @@ class CarapaceSpecManager:
431
433
  "name": d_name,
432
434
  "description": d_info.description or f"Kapsel {d_name} extension",
433
435
  })
434
- plugin_subcmds = [c for c in add_subcmds if c["name"] != "update"]
436
+ plugin_subcmds = [c for c in add_subcmds if c["name"] not in ("update", "all")]
435
437
  for cmd_entry in kapsel_commands:
436
438
  if cmd_entry["name"] == "add":
437
439
  cmd_entry["commands"] = add_subcmds
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kapsel-cli
3
- Version: 0.3.6
3
+ Version: 0.3.8
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kapsel-cli"
7
- version = "0.3.6"
7
+ version = "0.3.8"
8
8
  description = "Next-generation cross-platform terminal capsule & ergonomic shell multiplexer with context-aware command translation and 1,000+ CLI completions."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -40,3 +40,13 @@ def test_kps_add_calls_remote_fetcher_when_missing():
40
40
  ret = handle_add_command(["nonexistent_plugin_xyz"])
41
41
  assert ret == 1
42
42
  assert mock_fetch.called
43
+
44
+
45
+ def test_kapsel_add_all_batch():
46
+ """Verifies that 'kapsel add all' invokes batch installation sequentially."""
47
+ with patch("kapsel.completion.kps.builtins.add._install_single_plugin") as mock_single_install:
48
+ mock_single_install.return_value = (True, "Installed")
49
+ ret = handle_add_command(["all"])
50
+ assert ret == 0
51
+ assert mock_single_install.call_count >= 10
52
+
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes