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.
Files changed (130) hide show
  1. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/PKG-INFO +1 -1
  2. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/__init__.py +1 -1
  3. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/add.py +3 -3
  4. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/plugin_switch.py +2 -1
  5. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/catalog.py +22 -35
  6. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/fetcher.py +48 -12
  7. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/tools/installer.py +92 -39
  8. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/tools/registry.py +20 -1
  9. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/PKG-INFO +1 -1
  10. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/pyproject.toml +1 -1
  11. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_install.py +27 -2
  12. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/README.md +0 -0
  13. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/__main__.py +0 -0
  14. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/cli.py +0 -0
  15. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/__init__.py +0 -0
  16. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/carapace_engine.py +0 -0
  17. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/carapace_installer.py +0 -0
  18. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/completer.py +0 -0
  19. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/__init__.py +0 -0
  20. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/__init__.py +0 -0
  21. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/completion.py +0 -0
  22. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/config.py +0 -0
  23. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/datadir.py +0 -0
  24. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/dir_nav.py +0 -0
  25. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/help.py +0 -0
  26. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/language.py +0 -0
  27. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/restart.py +0 -0
  28. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/search.py +0 -0
  29. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/status.py +0 -0
  30. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/toggle.py +0 -0
  31. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/builtins/upgrade.py +0 -0
  32. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/dispatcher.py +0 -0
  33. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/kps/registry.py +0 -0
  34. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/completion/spec_manager.py +0 -0
  35. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/__init__.py +0 -0
  36. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/block/__init__.py +0 -0
  37. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/block/model.py +0 -0
  38. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/block/registry.py +0 -0
  39. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/block/runner.py +0 -0
  40. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/detector.py +0 -0
  41. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/engine.py +0 -0
  42. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/executor.py +0 -0
  43. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/i18n.py +0 -0
  44. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/__init__.py +0 -0
  45. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/base.py +0 -0
  46. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/context.py +0 -0
  47. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/hooks.py +0 -0
  48. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/plugin/manager.py +0 -0
  49. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/tools/__init__.py +0 -0
  50. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/core/update_check.py +0 -0
  51. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/i18n.py +0 -0
  52. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
  53. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
  54. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/de/help.yaml +0 -0
  55. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/de/tips.yaml +0 -0
  56. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/en/help.yaml +0 -0
  57. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/en/tips.yaml +0 -0
  58. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
  59. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
  60. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/es/help.yaml +0 -0
  61. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/es/tips.yaml +0 -0
  62. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
  63. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
  64. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/fr/help.yaml +0 -0
  65. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/fr/tips.yaml +0 -0
  66. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
  67. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
  68. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ja/help.yaml +0 -0
  69. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ja/tips.yaml +0 -0
  70. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/kapsel.pot +0 -0
  71. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
  72. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
  73. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ru/help.yaml +0 -0
  74. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/ru/tips.yaml +0 -0
  75. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
  76. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
  77. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/zh_CN/help.yaml +0 -0
  78. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/locales/zh_CN/tips.yaml +0 -0
  79. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/storage/__init__.py +0 -0
  80. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/storage/config.py +0 -0
  81. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/storage/history.py +0 -0
  82. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/storage/logger.py +0 -0
  83. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/storage/migrate.py +0 -0
  84. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/__init__.py +0 -0
  85. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/banner.py +0 -0
  86. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/card.py +0 -0
  87. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/prompt.py +0 -0
  88. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/theme.py +0 -0
  89. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/tips.py +0 -0
  90. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel/ui/viewport.py +0 -0
  91. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/SOURCES.txt +0 -0
  92. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/dependency_links.txt +0 -0
  93. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/entry_points.txt +0 -0
  94. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/requires.txt +0 -0
  95. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/kapsel_cli.egg-info/top_level.txt +0 -0
  96. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/setup.cfg +0 -0
  97. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_add_concurrent.py +0 -0
  98. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_ai_spec_sanitizer.py +0 -0
  99. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_block_system.py +0 -0
  100. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_carapace_installer.py +0 -0
  101. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_config.py +0 -0
  102. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_ctrl_b_roaming.py +0 -0
  103. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_dir_nav.py +0 -0
  104. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_history.py +0 -0
  105. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_i18n.py +0 -0
  106. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_kp_enhanced.py +0 -0
  107. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_multiline_passthrough.py +0 -0
  108. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_ai.py +0 -0
  109. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_alias.py +0 -0
  110. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_autopilot.py +0 -0
  111. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_backup.py +0 -0
  112. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_commit.py +0 -0
  113. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_fetcher.py +0 -0
  114. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_help.py +0 -0
  115. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_init.py +0 -0
  116. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_install_batch.py +0 -0
  117. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_kssh.py +0 -0
  118. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_lang.py +0 -0
  119. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_mend.py +0 -0
  120. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_portal.py +0 -0
  121. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_preview.py +0 -0
  122. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_queue.py +0 -0
  123. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_template.py +0 -0
  124. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_plugin_trans.py +0 -0
  125. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_restart_and_pypi.py +0 -0
  126. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_search_and_completions.py +0 -0
  127. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_spec_manager.py +0 -0
  128. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_toggle.py +0 -0
  129. {kapsel_cli-0.7.2 → kapsel_cli-0.7.6}/tests/test_tools_registry.py +0 -0
  130. {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.2
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
@@ -3,4 +3,4 @@ Kapsel: Cross-platform adaptive smart terminal capsule.
3
3
  Wrap complexity, expose simplicity.
4
4
  """
5
5
 
6
- __version__ = "0.7.2"
6
+ __version__ = "0.7.6"
@@ -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
- dev_plugin_dir = Path.cwd() / "plugins" / target_name
132
+ repo_plugin_dir = Path(__file__).resolve().parent.parent.parent.parent.parent / "plugins" / target_name
133
133
 
134
- if is_dev and dev_plugin_dir.exists() and dev_plugin_dir.is_dir() and (dev_plugin_dir / "__init__.py").exists():
135
- shutil.copytree(dev_plugin_dir, dest_plugin_dir, dirs_exist_ok=True)
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. Global runtime plugins directory
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
- # 4. Global root catalog fallback
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
- # If file not present yet, try to fetch from remote repository before falling back to defaults
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.2.0" if k in ("alias", "install") else "0.1.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 [OFFICIAL_PLUGINS_REPO_GIT, OFFICIAL_PLUGINS_REPO_MIRROR_GIT]:
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=20.0,
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 [(OFFICIAL_ARCHIVE_URL, "GitHub"), (OFFICIAL_ARCHIVE_MIRROR_URL, "Mirror")]:
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=15.0) as resp:
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
- for url, label in [
177
- (OFFICIAL_CATALOG_URL, "GitHub"),
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=8.0) as resp:
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 local cache so subsequent calls skip network
188
- global_cache = get_kapsel_dir() / "plugins_catalog.json"
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
- bin_name = tool_def.get("bin", tool_name) if tool_def else tool_name
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
- # 2. Local Kapsel bin directory
69
- local_target = b_dir / exe_name
70
- if local_target.exists():
71
- return str(local_target)
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
- # 3. Windows-specific typical locations
74
- if is_win:
75
- user_home = Path(os.environ.get("USERPROFILE", Path.home()))
76
- candidates = [
77
- user_home / "scoop" / "shims" / exe_name,
78
- user_home / "scoop" / "apps" / bin_name / "current" / exe_name,
79
- user_home / "AppData" / "Local" / "Microsoft" / "WinGet" / "Links" / exe_name,
80
- user_home / ".cargo" / "bin" / exe_name,
81
- ]
82
- for cand in candidates:
83
- if cand.is_file():
84
- return str(cand)
85
- else:
86
- # 4. Unix-specific typical locations
87
- user_home = Path(os.environ.get("HOME", Path.home()))
88
- candidates = [
89
- Path("/opt/homebrew/bin") / exe_name,
90
- Path("/usr/local/bin") / exe_name,
91
- Path("/usr/bin") / exe_name,
92
- user_home / ".cargo" / "bin" / exe_name,
93
- ]
94
- for cand in candidates:
95
- if cand.is_file():
96
- return str(cand)
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
- is_win = sys.platform == "win32"
224
- bin_name = tool_def.get("bin", tool_def.get("name", "tool"))
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
- if tool_def.get("bin") == tool_name or tool_def.get("name") == tool_name:
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.2
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.2"
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>" in out
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