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.
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/PKG-INFO +1 -1
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/__init__.py +1 -1
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/add.py +132 -51
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/upgrade.py +11 -9
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/spec_manager.py +5 -3
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/PKG-INFO +1 -1
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/pyproject.toml +1 -1
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_fetcher.py +10 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/README.md +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/__main__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/cli.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/carapace_engine.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/carapace_installer.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/completer.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/completion.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/config.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/datadir.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/help.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/language.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/plugin_switch.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/search.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/status.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/builtins/toggle.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/dispatcher.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/completion/kps/registry.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/block/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/block/model.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/block/registry.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/block/runner.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/detector.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/engine.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/executor.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/i18n.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/base.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/catalog.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/context.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/fetcher.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/hooks.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/core/plugin/manager.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/i18n.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/de/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/de/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/en/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/en/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/es/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/es/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/fr/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/fr/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ja/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ja/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/kapsel.pot +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ru/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/ru/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/zh_CN/help.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/locales/zh_CN/tips.yaml +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/storage/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/storage/config.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/storage/history.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/storage/logger.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/storage/migrate.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/__init__.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/banner.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/card.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/prompt.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/theme.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/tips.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel/ui/viewport.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/SOURCES.txt +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/dependency_links.txt +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/entry_points.txt +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/requires.txt +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/kapsel_cli.egg-info/top_level.txt +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/setup.cfg +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_block_system.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_carapace_installer.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_config.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_history.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_i18n.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_ai.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_alias.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_autopilot.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_init.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_install.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_plugin_portal.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_search_and_completions.py +0 -0
- {kapsel_cli-0.3.6 → kapsel_cli-0.3.8}/tests/test_spec_manager.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
91
|
-
con.print(f"[
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
135
|
-
res["published_at"] = gh_data.get("published_at"
|
|
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
|
|
144
|
-
latest_ver = info.get("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": "
|
|
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
|
|
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"]
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|