kapsel-cli 0.7.2__tar.gz → 0.7.6__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.7.2 → kapsel_cli-0.7.6}/PKG-INFO +1 -1
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/__init__.py +1 -1
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/add.py +3 -3
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/plugin_switch.py +2 -1
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/catalog.py +22 -35
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/fetcher.py +48 -12
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/tools/installer.py +92 -39
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/tools/registry.py +20 -1
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/PKG-INFO +1 -1
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/pyproject.toml +1 -1
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_install.py +27 -2
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/README.md +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/__main__.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/cli.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/__init__.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/carapace_engine.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/carapace_installer.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/completer.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/__init__.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/__init__.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/completion.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/config.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/datadir.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/dir_nav.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/help.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/language.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/restart.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/search.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/status.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/toggle.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/upgrade.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/dispatcher.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/registry.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/spec_manager.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/__init__.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/block/__init__.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/block/model.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/block/registry.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/block/runner.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/detector.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/engine.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/executor.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/i18n.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/__init__.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/base.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/context.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/hooks.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/manager.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/tools/__init__.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/update_check.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/i18n.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/de/help.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/de/tips.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/en/help.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/en/tips.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/es/help.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/es/tips.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/fr/help.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/fr/tips.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ja/help.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ja/tips.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/kapsel.pot +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ru/help.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ru/tips.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/zh_CN/help.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/zh_CN/tips.yaml +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/storage/__init__.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/storage/config.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/storage/history.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/storage/logger.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/storage/migrate.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/__init__.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/banner.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/card.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/prompt.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/theme.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/tips.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/viewport.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/SOURCES.txt +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/dependency_links.txt +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/entry_points.txt +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/requires.txt +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/top_level.txt +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/setup.cfg +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_add_concurrent.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_ai_spec_sanitizer.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_block_system.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_carapace_installer.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_config.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_ctrl_b_roaming.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_dir_nav.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_history.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_i18n.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_kp_enhanced.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_multiline_passthrough.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_ai.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_alias.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_autopilot.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_backup.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_commit.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_fetcher.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_help.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_init.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_install_batch.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_kssh.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_lang.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_mend.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_portal.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_preview.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_queue.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_template.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_trans.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_restart_and_pypi.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_search_and_completions.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_spec_manager.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_toggle.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_tools_registry.py +0 -0
- {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_upgrade.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kapsel-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.6
|
|
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
|
|
@@ -129,10 +129,10 @@ def _install_single_plugin(
|
|
|
129
129
|
else:
|
|
130
130
|
target_name = target_raw.lower()
|
|
131
131
|
dest_plugin_dir = global_plugins_dir / target_name
|
|
132
|
-
|
|
132
|
+
repo_plugin_dir = Path(__file__).resolve().parent.parent.parent.parent.parent / "plugins" / target_name
|
|
133
133
|
|
|
134
|
-
if
|
|
135
|
-
shutil.copytree(
|
|
134
|
+
if repo_plugin_dir.exists() and repo_plugin_dir.is_dir() and (repo_plugin_dir / "__init__.py").exists():
|
|
135
|
+
shutil.copytree(repo_plugin_dir, dest_plugin_dir, dirs_exist_ok=True)
|
|
136
136
|
plugin_dir = dest_plugin_dir
|
|
137
137
|
elif dest_plugin_dir.exists() and dest_plugin_dir.is_dir():
|
|
138
138
|
plugin_dir = dest_plugin_dir
|
|
@@ -121,8 +121,9 @@ def handle_enable_plugin(args: List[str], console: Optional[Console] = None) ->
|
|
|
121
121
|
global_plugins_dir = get_kapsel_dir() / "plugins"
|
|
122
122
|
dest_plugin_dir = global_plugins_dir / target
|
|
123
123
|
local_dev_dir = Path.cwd() / "plugins" / target
|
|
124
|
+
repo_plugin_dir = Path(__file__).resolve().parent.parent.parent.parent.parent / "plugins" / target
|
|
124
125
|
|
|
125
|
-
if not (dest_plugin_dir.exists() or local_dev_dir.exists()):
|
|
126
|
+
if not (dest_plugin_dir.exists() or local_dev_dir.exists() or repo_plugin_dir.exists()):
|
|
126
127
|
con.print(f"[dim]Plugin '[white]{target}[/]' is not installed locally. Fetching from official repository...[/]")
|
|
127
128
|
from kapsel.core.plugin.fetcher import fetch_plugin_from_remote
|
|
128
129
|
if not fetch_plugin_from_remote(target, dest_plugin_dir, con):
|
|
@@ -18,20 +18,29 @@ DEFAULT_CATALOG: Dict[str, str] = {
|
|
|
18
18
|
"update": "Scan and update available plugin catalog and completion dictionary",
|
|
19
19
|
"ai": "Terminal AI assistant and setup wizard (aichat)",
|
|
20
20
|
"alias": "Cross-platform Linux command alias mapper",
|
|
21
|
+
"aria2": "Concurrent download accelerator powered by aria2c (-s 16, -x 16, -j 4)",
|
|
21
22
|
"autopilot": "Production process manager and daemon supervisor (PM2)",
|
|
23
|
+
"backup": "Zero-pollution folder version backup and snapshot restore (gitkeep-cli)",
|
|
24
|
+
"commit": "Offline rule-based Conventional Commit deduction and interactive git commit manager",
|
|
25
|
+
"dispatch": "Remote execution and cluster management engine",
|
|
22
26
|
"fuck": "Intelligent console command error correction (thefuck)",
|
|
23
27
|
"help": "Fast interactive command cheat sheets (tealdeer)",
|
|
24
28
|
"init": "Project polyglot development environment and toolchain runtime initializer (mise)",
|
|
25
29
|
"install": "Unified cross-platform package manager (mpm)",
|
|
30
|
+
"kssh": "Cross-platform remote terminal connection and roaming manager",
|
|
31
|
+
"lang": "Instant terminal language, shell culture, and CLI localization switcher",
|
|
32
|
+
"mend": "Terminal environment diagnostics, doctor checks, and dependency repair",
|
|
26
33
|
"portal": "Smart directory teleportation and workspace navigator (zoxide)",
|
|
34
|
+
"preview": "Smart terminal preview dispatcher (bat, glow, jq, xsv, eza, 7z/7zz, chafa) and file manager (fm)",
|
|
27
35
|
"profile": "Dotfiles and workspace sync roaming (chezmoi)",
|
|
28
36
|
"queue": "Background task queue and autonomous execution (Pueue)",
|
|
29
37
|
"rec": "Snippet recorder and interactive runner (pet)",
|
|
30
38
|
"shore": "Fast intelligent mirror source switcher (chsrc)",
|
|
39
|
+
"template": "Project boilerplate initialization and code templating engine",
|
|
40
|
+
"trans": "Command output translator powered by fanyi and translate-shell",
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
|
|
34
|
-
|
|
35
44
|
def _resolve_catalog_paths():
|
|
36
45
|
"""Returns candidate paths for catalog.json in order of priority."""
|
|
37
46
|
paths = []
|
|
@@ -53,12 +62,20 @@ def _resolve_catalog_paths():
|
|
|
53
62
|
if workspace_catalog not in paths:
|
|
54
63
|
paths.append(workspace_catalog)
|
|
55
64
|
|
|
56
|
-
# 3.
|
|
65
|
+
# 3. Kapsel repository root plugins/catalog.json (independent of CWD)
|
|
66
|
+
try:
|
|
67
|
+
repo_catalog = Path(__file__).resolve().parent.parent.parent.parent / "plugins" / "catalog.json"
|
|
68
|
+
if repo_catalog.is_file() and repo_catalog not in paths:
|
|
69
|
+
paths.append(repo_catalog)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# 4. Global runtime plugins directory (~/.kapsel/plugins/catalog.json)
|
|
57
74
|
global_in_plugins = get_kapsel_dir() / "plugins" / "catalog.json"
|
|
58
75
|
if global_in_plugins not in paths:
|
|
59
76
|
paths.append(global_in_plugins)
|
|
60
77
|
|
|
61
|
-
#
|
|
78
|
+
# 5. Global root catalog fallback (~/.kapsel/catalog.json)
|
|
62
79
|
global_root = get_kapsel_dir() / "catalog.json"
|
|
63
80
|
if global_root not in paths:
|
|
64
81
|
paths.append(global_root)
|
|
@@ -111,40 +128,10 @@ def load_plugin_catalog_rich() -> Dict[str, Dict[str, Any]]:
|
|
|
111
128
|
except Exception:
|
|
112
129
|
pass
|
|
113
130
|
|
|
114
|
-
#
|
|
115
|
-
try:
|
|
116
|
-
from kapsel.core.plugin.fetcher import fetch_remote_catalog
|
|
117
|
-
remote_data = fetch_remote_catalog()
|
|
118
|
-
if remote_data and isinstance(remote_data, dict):
|
|
119
|
-
result = {}
|
|
120
|
-
for k, v in remote_data.items():
|
|
121
|
-
if k.startswith(("_", "$")):
|
|
122
|
-
continue
|
|
123
|
-
if isinstance(v, dict):
|
|
124
|
-
result[k] = {
|
|
125
|
-
"version": v.get("version", "0.1.0"),
|
|
126
|
-
"description": v.get("description", f"Plugin {k}"),
|
|
127
|
-
"changelog": v.get("changelog", ""),
|
|
128
|
-
}
|
|
129
|
-
else:
|
|
130
|
-
result[k] = {
|
|
131
|
-
"version": "0.1.0",
|
|
132
|
-
"description": str(v),
|
|
133
|
-
"changelog": "",
|
|
134
|
-
}
|
|
135
|
-
result.setdefault("update", {
|
|
136
|
-
"version": "0.1.0",
|
|
137
|
-
"description": "Scan and update available plugin catalog and completion dictionary",
|
|
138
|
-
"changelog": "Catalog dictionary update scanner.",
|
|
139
|
-
})
|
|
140
|
-
return result
|
|
141
|
-
except Exception:
|
|
142
|
-
pass
|
|
143
|
-
|
|
144
|
-
# Final fallback: built-in DEFAULT_CATALOG (offline / import error)
|
|
131
|
+
# Purely local fallback: built-in DEFAULT_CATALOG (0ms, zero network requests)
|
|
145
132
|
return {
|
|
146
133
|
k: {
|
|
147
|
-
"version": "0.
|
|
134
|
+
"version": "0.1.0",
|
|
148
135
|
"description": v,
|
|
149
136
|
"changelog": "",
|
|
150
137
|
}
|
|
@@ -25,23 +25,44 @@ OFFICIAL_ARCHIVE_URL = "https://github.com/MrEiu/plugins/archive/refs/heads/mast
|
|
|
25
25
|
OFFICIAL_ARCHIVE_MIRROR_URL = "https://ghproxy.net/https://github.com/MrEiu/plugins/archive/refs/heads/master.tar.gz"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def is_github_reachable(timeout: float = 1.0) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Quickly tests connectivity to GitHub with a strict timeout of at most 1 second.
|
|
31
|
+
If unreachable or timeout expires, returns False so caller immediately falls back to mirror.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
req = urllib.request.Request("https://github.com", headers={"User-Agent": "Kapsel-Probe/1.0"})
|
|
35
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
36
|
+
return resp.status in (200, 301, 302)
|
|
37
|
+
except Exception:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
28
41
|
def _fetch_via_git(plugin_name: str, dest_dir: Path, console: Console) -> bool:
|
|
29
42
|
"""Attempts to fetch a plugin using git shallow clone."""
|
|
30
43
|
if not shutil.which("git"):
|
|
31
44
|
return False
|
|
32
45
|
|
|
46
|
+
# Probe GitHub connectivity with 1s limit; if unreachable, immediately switch to mirror
|
|
47
|
+
use_mirror = not is_github_reachable(timeout=1.0)
|
|
48
|
+
repo_urls = (
|
|
49
|
+
[(OFFICIAL_PLUGINS_REPO_MIRROR_GIT, "Mirror (ghproxy)"), (OFFICIAL_PLUGINS_REPO_GIT, "GitHub")]
|
|
50
|
+
if use_mirror
|
|
51
|
+
else [(OFFICIAL_PLUGINS_REPO_GIT, "GitHub"), (OFFICIAL_PLUGINS_REPO_MIRROR_GIT, "Mirror (ghproxy)")]
|
|
52
|
+
)
|
|
53
|
+
|
|
33
54
|
with tempfile.TemporaryDirectory(prefix="kapsel_plugin_git_") as tmp_str:
|
|
34
55
|
tmp_path = Path(tmp_str)
|
|
35
|
-
console.print(f"[dim]Attempting Git clone from {OFFICIAL_PLUGINS_REPO_GIT}...[/]")
|
|
36
56
|
|
|
37
57
|
cloned = False
|
|
38
|
-
for repo_url in
|
|
58
|
+
for repo_url, label in repo_urls:
|
|
39
59
|
try:
|
|
60
|
+
console.print(f"[dim]Attempting Git clone via {label}...[/]")
|
|
40
61
|
res = subprocess.run(
|
|
41
62
|
["git", "clone", "--depth", "1", repo_url, str(tmp_path / "repo")],
|
|
42
63
|
capture_output=True,
|
|
43
64
|
text=True,
|
|
44
|
-
timeout=
|
|
65
|
+
timeout=15.0,
|
|
45
66
|
)
|
|
46
67
|
if res.returncode == 0:
|
|
47
68
|
cloned = True
|
|
@@ -65,14 +86,22 @@ def _fetch_via_archive(plugin_name: str, dest_dir: Path, console: Console) -> bo
|
|
|
65
86
|
"""Attempts to fetch a plugin by streaming and extracting the GitHub tarball."""
|
|
66
87
|
console.print(f"[dim]Downloading plugin package from remote repository...[/]")
|
|
67
88
|
|
|
89
|
+
# Probe GitHub connectivity with 1s limit; if unreachable, immediately switch to mirror
|
|
90
|
+
use_mirror = not is_github_reachable(timeout=1.0)
|
|
91
|
+
candidates = (
|
|
92
|
+
[(OFFICIAL_ARCHIVE_MIRROR_URL, "Mirror (ghproxy)"), (OFFICIAL_ARCHIVE_URL, "GitHub")]
|
|
93
|
+
if use_mirror
|
|
94
|
+
else [(OFFICIAL_ARCHIVE_URL, "GitHub"), (OFFICIAL_ARCHIVE_MIRROR_URL, "Mirror (ghproxy)")]
|
|
95
|
+
)
|
|
96
|
+
|
|
68
97
|
headers = {"User-Agent": "Kapsel-Plugin-Fetcher/1.0"}
|
|
69
98
|
archive_bytes: Optional[bytes] = None
|
|
70
99
|
|
|
71
|
-
for url, label in
|
|
100
|
+
for url, label in candidates:
|
|
72
101
|
try:
|
|
73
102
|
console.print(f"[dim]Connecting to {label}...[/]")
|
|
74
103
|
req = urllib.request.Request(url, headers=headers)
|
|
75
|
-
with urllib.request.urlopen(req, timeout=
|
|
104
|
+
with urllib.request.urlopen(req, timeout=10.0) as resp:
|
|
76
105
|
if resp.status == 200:
|
|
77
106
|
archive_bytes = resp.read()
|
|
78
107
|
break
|
|
@@ -173,25 +202,32 @@ def fetch_remote_catalog(console: Optional[Console] = None) -> Optional[dict]:
|
|
|
173
202
|
con = console or Console(legacy_windows=False)
|
|
174
203
|
headers = {"User-Agent": "Kapsel-Plugin-Fetcher/1.0"}
|
|
175
204
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
(OFFICIAL_CATALOG_MIRROR_URL, "Mirror"),
|
|
179
|
-
|
|
205
|
+
use_mirror = not is_github_reachable(timeout=1.0)
|
|
206
|
+
candidates = (
|
|
207
|
+
[(OFFICIAL_CATALOG_MIRROR_URL, "Mirror (ghproxy)"), (OFFICIAL_CATALOG_URL, "GitHub")]
|
|
208
|
+
if use_mirror
|
|
209
|
+
else [(OFFICIAL_CATALOG_URL, "GitHub"), (OFFICIAL_CATALOG_MIRROR_URL, "Mirror (ghproxy)")]
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
for url, label in candidates:
|
|
180
213
|
try:
|
|
181
214
|
req = urllib.request.Request(url, headers=headers)
|
|
182
|
-
with urllib.request.urlopen(req, timeout=
|
|
215
|
+
with urllib.request.urlopen(req, timeout=5.0) as resp:
|
|
183
216
|
if resp.status == 200:
|
|
184
217
|
raw = resp.read().decode("utf-8")
|
|
185
218
|
data = _json.loads(raw)
|
|
186
219
|
if isinstance(data, dict):
|
|
187
|
-
# Write to
|
|
188
|
-
global_cache = get_kapsel_dir() / "
|
|
220
|
+
# Write to global plugins catalog directory matching _resolve_catalog_paths
|
|
221
|
+
global_cache = get_kapsel_dir() / "plugins" / "catalog.json"
|
|
189
222
|
try:
|
|
190
223
|
global_cache.parent.mkdir(parents=True, exist_ok=True)
|
|
191
224
|
global_cache.write_text(
|
|
192
225
|
_json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
193
226
|
encoding="utf-8",
|
|
194
227
|
)
|
|
228
|
+
# Remove obsolete plugins_catalog.json if present
|
|
229
|
+
legacy_cache = get_kapsel_dir() / "plugins_catalog.json"
|
|
230
|
+
legacy_cache.unlink(missing_ok=True)
|
|
195
231
|
except Exception as cache_err:
|
|
196
232
|
logger.debug(f"Failed to write remote catalog cache: {cache_err}")
|
|
197
233
|
logger.debug(f"Remote catalog fetched from {label} ({len(data)} entries).")
|
|
@@ -48,52 +48,105 @@ def get_current_platform_keys() -> List[str]:
|
|
|
48
48
|
return keys
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def get_tool_bin_candidates(tool_def: Optional[Dict[str, Any]], fallback_name: str = "tool") -> List[str]:
|
|
52
|
+
"""
|
|
53
|
+
Extracts binary candidate names for the current platform from tool definition.
|
|
54
|
+
Supports string bin, dictionary bin per platform (e.g. windows: 7z, unix: 7zz),
|
|
55
|
+
platforms.<os>.bin, and common known aliases (e.g. 7z <-> 7zz).
|
|
56
|
+
Returns a prioritized list of binary name candidates.
|
|
57
|
+
"""
|
|
58
|
+
if not tool_def:
|
|
59
|
+
return [fallback_name]
|
|
60
|
+
|
|
61
|
+
candidates: List[str] = []
|
|
62
|
+
plat_keys = get_current_platform_keys()
|
|
63
|
+
|
|
64
|
+
# 1. Platform-specific bin in platforms section (e.g. platforms.unix.bin or platforms.windows.bin)
|
|
65
|
+
platforms_def = tool_def.get("platforms", {})
|
|
66
|
+
if isinstance(platforms_def, dict):
|
|
67
|
+
for key in reversed(plat_keys):
|
|
68
|
+
p_data = platforms_def.get(key)
|
|
69
|
+
if isinstance(p_data, dict) and "bin" in p_data:
|
|
70
|
+
b = p_data["bin"]
|
|
71
|
+
if isinstance(b, str) and b and b not in candidates:
|
|
72
|
+
candidates.append(b)
|
|
73
|
+
|
|
74
|
+
# 2. Tool-level 'bin' attribute (dict or str)
|
|
75
|
+
raw_bin = tool_def.get("bin")
|
|
76
|
+
if isinstance(raw_bin, dict):
|
|
77
|
+
for key in reversed(plat_keys):
|
|
78
|
+
if key in raw_bin:
|
|
79
|
+
b = raw_bin[key]
|
|
80
|
+
if isinstance(b, str) and b and b not in candidates:
|
|
81
|
+
candidates.append(b)
|
|
82
|
+
elif isinstance(raw_bin, str) and raw_bin:
|
|
83
|
+
if raw_bin not in candidates:
|
|
84
|
+
candidates.append(raw_bin)
|
|
85
|
+
|
|
86
|
+
# 3. Tool name fallback
|
|
87
|
+
t_name = tool_def.get("name")
|
|
88
|
+
if isinstance(t_name, str) and t_name and t_name not in candidates:
|
|
89
|
+
candidates.append(t_name)
|
|
90
|
+
|
|
91
|
+
if not candidates:
|
|
92
|
+
candidates.append(fallback_name)
|
|
93
|
+
|
|
94
|
+
# 4. Canonical tool aliases (e.g. 7z <-> 7zz)
|
|
95
|
+
if "7z" in candidates and "7zz" not in candidates:
|
|
96
|
+
candidates.append("7zz")
|
|
97
|
+
elif "7zz" in candidates and "7z" not in candidates:
|
|
98
|
+
candidates.append("7z")
|
|
99
|
+
|
|
100
|
+
return candidates
|
|
101
|
+
|
|
102
|
+
|
|
51
103
|
def resolve_tool_executable(tool_name: str, bin_dir: Optional[Path] = None) -> Optional[str]:
|
|
52
104
|
"""
|
|
53
105
|
Resolves the executable path of a tool if installed in PATH,
|
|
54
106
|
Kapsel bin, Scoop shims, WinGet links, or Cargo bin.
|
|
55
107
|
"""
|
|
56
108
|
tool_def = get_tool(tool_name)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# 1. Standard PATH
|
|
60
|
-
found = shutil.which(bin_name)
|
|
61
|
-
if found:
|
|
62
|
-
return found
|
|
63
|
-
|
|
64
|
-
is_win = sys.platform == "win32"
|
|
65
|
-
exe_name = f"{bin_name}.exe" if is_win else bin_name
|
|
109
|
+
bin_candidates = get_tool_bin_candidates(tool_def, fallback_name=tool_name)
|
|
66
110
|
b_dir = bin_dir or get_kapsel_bin_dir()
|
|
111
|
+
is_win = sys.platform == "win32"
|
|
67
112
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
for bin_name in bin_candidates:
|
|
114
|
+
# 1. Standard PATH
|
|
115
|
+
found = shutil.which(bin_name)
|
|
116
|
+
if found:
|
|
117
|
+
return found
|
|
72
118
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
exe_name = f"{bin_name}.exe" if is_win else bin_name
|
|
120
|
+
|
|
121
|
+
# 2. Local Kapsel bin directory
|
|
122
|
+
local_target = b_dir / exe_name
|
|
123
|
+
if local_target.exists():
|
|
124
|
+
return str(local_target)
|
|
125
|
+
|
|
126
|
+
# 3. Windows-specific typical locations
|
|
127
|
+
if is_win:
|
|
128
|
+
user_home = Path(os.environ.get("USERPROFILE", Path.home()))
|
|
129
|
+
candidates = [
|
|
130
|
+
user_home / "scoop" / "shims" / exe_name,
|
|
131
|
+
user_home / "scoop" / "apps" / bin_name / "current" / exe_name,
|
|
132
|
+
user_home / "AppData" / "Local" / "Microsoft" / "WinGet" / "Links" / exe_name,
|
|
133
|
+
user_home / ".cargo" / "bin" / exe_name,
|
|
134
|
+
]
|
|
135
|
+
for cand in candidates:
|
|
136
|
+
if cand.is_file():
|
|
137
|
+
return str(cand)
|
|
138
|
+
else:
|
|
139
|
+
# 4. Unix-specific typical locations
|
|
140
|
+
user_home = Path(os.environ.get("HOME", Path.home()))
|
|
141
|
+
candidates = [
|
|
142
|
+
Path("/opt/homebrew/bin") / exe_name,
|
|
143
|
+
Path("/usr/local/bin") / exe_name,
|
|
144
|
+
Path("/usr/bin") / exe_name,
|
|
145
|
+
user_home / ".cargo" / "bin" / exe_name,
|
|
146
|
+
]
|
|
147
|
+
for cand in candidates:
|
|
148
|
+
if cand.is_file():
|
|
149
|
+
return str(cand)
|
|
97
150
|
|
|
98
151
|
return None
|
|
99
152
|
|
|
@@ -220,8 +273,8 @@ class ToolInstaller:
|
|
|
220
273
|
return False
|
|
221
274
|
|
|
222
275
|
dest = strategy.get("dest")
|
|
223
|
-
|
|
224
|
-
bin_name =
|
|
276
|
+
bin_candidates = get_tool_bin_candidates(tool_def, fallback_name=tool_def.get("name", "tool"))
|
|
277
|
+
bin_name = bin_candidates[0]
|
|
225
278
|
exe_name = f"{bin_name}.exe" if is_win else bin_name
|
|
226
279
|
|
|
227
280
|
target_file = self.bin_dir / exe_name
|
|
@@ -146,9 +146,28 @@ class ToolRegistry:
|
|
|
146
146
|
return all_tools[tool_name]
|
|
147
147
|
|
|
148
148
|
for _, tool_def in all_tools.items():
|
|
149
|
-
|
|
149
|
+
b = tool_def.get("bin")
|
|
150
|
+
if isinstance(b, dict):
|
|
151
|
+
if tool_name in b.values():
|
|
152
|
+
return tool_def
|
|
153
|
+
elif b == tool_name:
|
|
150
154
|
return tool_def
|
|
151
155
|
|
|
156
|
+
if tool_def.get("name") == tool_name:
|
|
157
|
+
return tool_def
|
|
158
|
+
|
|
159
|
+
platforms = tool_def.get("platforms", {})
|
|
160
|
+
if isinstance(platforms, dict):
|
|
161
|
+
for _, p_data in platforms.items():
|
|
162
|
+
if isinstance(p_data, dict) and p_data.get("bin") == tool_name:
|
|
163
|
+
return tool_def
|
|
164
|
+
|
|
165
|
+
# Canonical aliases (e.g. 7zz <-> 7z)
|
|
166
|
+
if tool_name == "7zz" and "7z" in all_tools:
|
|
167
|
+
return all_tools["7z"]
|
|
168
|
+
if tool_name == "7z" and "7zz" in all_tools:
|
|
169
|
+
return all_tools["7zz"]
|
|
170
|
+
|
|
152
171
|
return None
|
|
153
172
|
|
|
154
173
|
def sync_aggregated_file(self, target_path: Optional[Path] = None) -> Path:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kapsel-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.6
|
|
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.7.
|
|
7
|
+
version = "0.7.6"
|
|
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"
|
|
@@ -185,7 +185,7 @@ def test_handle_install_subcommands(tmp_path):
|
|
|
185
185
|
ret = plugin.handle_install(["--help"], con)
|
|
186
186
|
assert ret == 0
|
|
187
187
|
out = con.export_text()
|
|
188
|
-
assert "kps install <package_name
|
|
188
|
+
assert "kps install <package_name" in out
|
|
189
189
|
|
|
190
190
|
|
|
191
191
|
def test_handle_install_forwarding():
|
|
@@ -198,7 +198,7 @@ def test_handle_install_forwarding():
|
|
|
198
198
|
mock_run.return_value = 0
|
|
199
199
|
ret = plugin.handle_install(["curl"], con)
|
|
200
200
|
assert ret == 0
|
|
201
|
-
mock_run.assert_called_once_with("install", ["--winget", "--scoop", "curl"], con)
|
|
201
|
+
mock_run.assert_called_once_with("install", ["--winget", "--scoop", "curl"], con, custom_paths={})
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
def test_search_item_weights_and_sorting():
|
|
@@ -289,3 +289,28 @@ def test_build_results_table_top_3_grouping():
|
|
|
289
289
|
# Verify table columns exist and rows represent scoop and top-3 winget plus expand row
|
|
290
290
|
assert len(table.rows) == 1 + 3 + 1 # 1 scoop + 3 winget + 1 expandable row
|
|
291
291
|
|
|
292
|
+
|
|
293
|
+
def test_order_subcommand_interactive_dispatch(tmp_path):
|
|
294
|
+
"""Verify 'kps install order' dispatches to run_order_interactive when isatty is True."""
|
|
295
|
+
plugin = InstallPlugin()
|
|
296
|
+
mock_context = MagicMock()
|
|
297
|
+
mock_context.plugin_data_dir = tmp_path
|
|
298
|
+
plugin.context = mock_context
|
|
299
|
+
con = Console(record=True)
|
|
300
|
+
|
|
301
|
+
with patch("sys.stdin.isatty", return_value=True):
|
|
302
|
+
with patch("plugins.install.order_ui.run_order_interactive", return_value=0) as mock_ui:
|
|
303
|
+
ret = plugin.handle_install(["order"], con)
|
|
304
|
+
assert ret == 0
|
|
305
|
+
mock_ui.assert_called_once_with(plugin, con)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_order_ui_info_resolution():
|
|
309
|
+
"""Verify _get_manager_info returns executable status."""
|
|
310
|
+
from plugins.install.order_ui import _get_manager_info
|
|
311
|
+
# Test custom path
|
|
312
|
+
info, present = _get_manager_info("custom_m", {"custom_m": str(Path(__file__))})
|
|
313
|
+
assert present is True
|
|
314
|
+
assert "test_plugin_install.py" in info
|
|
315
|
+
|
|
316
|
+
|
|
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
|
|
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
|