kapsel-cli 0.1.0__py3-none-any.whl

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 (66) hide show
  1. kapsel/__init__.py +6 -0
  2. kapsel/cli.py +162 -0
  3. kapsel/completion/__init__.py +23 -0
  4. kapsel/completion/carapace_engine.py +236 -0
  5. kapsel/completion/completer.py +251 -0
  6. kapsel/completion/fig_engine.py +215 -0
  7. kapsel/completion/fig_schema.py +135 -0
  8. kapsel/completion/importer.py +92 -0
  9. kapsel/completion/kps/__init__.py +18 -0
  10. kapsel/completion/kps/builtins/__init__.py +92 -0
  11. kapsel/completion/kps/builtins/add.py +121 -0
  12. kapsel/completion/kps/builtins/config.py +184 -0
  13. kapsel/completion/kps/builtins/datadir.py +108 -0
  14. kapsel/completion/kps/builtins/help.py +270 -0
  15. kapsel/completion/kps/builtins/language.py +83 -0
  16. kapsel/completion/kps/builtins/status.py +121 -0
  17. kapsel/completion/kps/builtins/toggle.py +34 -0
  18. kapsel/completion/kps/dispatcher.py +65 -0
  19. kapsel/completion/kps/registry.py +101 -0
  20. kapsel/core/__init__.py +28 -0
  21. kapsel/core/detector.py +162 -0
  22. kapsel/core/engine.py +159 -0
  23. kapsel/core/executor.py +200 -0
  24. kapsel/core/i18n.py +31 -0
  25. kapsel/core/plugin/__init__.py +17 -0
  26. kapsel/core/plugin/base.py +53 -0
  27. kapsel/core/plugin/catalog.py +187 -0
  28. kapsel/core/plugin/context.py +71 -0
  29. kapsel/core/plugin/hooks.py +34 -0
  30. kapsel/core/plugin/manager.py +173 -0
  31. kapsel/i18n.py +196 -0
  32. kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
  33. kapsel/locales/de/LC_MESSAGES/kapsel.po +215 -0
  34. kapsel/locales/de/help.yaml +101 -0
  35. kapsel/locales/en/help.yaml +101 -0
  36. kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
  37. kapsel/locales/es/LC_MESSAGES/kapsel.po +213 -0
  38. kapsel/locales/es/help.yaml +101 -0
  39. kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
  40. kapsel/locales/fr/LC_MESSAGES/kapsel.po +213 -0
  41. kapsel/locales/fr/help.yaml +101 -0
  42. kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
  43. kapsel/locales/ja/LC_MESSAGES/kapsel.po +211 -0
  44. kapsel/locales/ja/help.yaml +101 -0
  45. kapsel/locales/kapsel.pot +210 -0
  46. kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
  47. kapsel/locales/ru/LC_MESSAGES/kapsel.po +212 -0
  48. kapsel/locales/ru/help.yaml +101 -0
  49. kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
  50. kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +211 -0
  51. kapsel/locales/zh_CN/help.yaml +101 -0
  52. kapsel/storage/__init__.py +17 -0
  53. kapsel/storage/config.py +362 -0
  54. kapsel/storage/history.py +166 -0
  55. kapsel/storage/logger.py +75 -0
  56. kapsel/storage/migrate.py +119 -0
  57. kapsel/ui/__init__.py +21 -0
  58. kapsel/ui/banner.py +121 -0
  59. kapsel/ui/card.py +109 -0
  60. kapsel/ui/prompt.py +391 -0
  61. kapsel/ui/theme.py +59 -0
  62. kapsel_cli-0.1.0.dist-info/METADATA +397 -0
  63. kapsel_cli-0.1.0.dist-info/RECORD +66 -0
  64. kapsel_cli-0.1.0.dist-info/WHEEL +5 -0
  65. kapsel_cli-0.1.0.dist-info/entry_points.txt +3 -0
  66. kapsel_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,121 @@
1
+ """
2
+ Kapsel Status Command.
3
+ Renders comprehensive runtime environment, shell sniffing, and sandbox status dashboard.
4
+ """
5
+
6
+ from datetime import datetime
7
+ import os
8
+ from pathlib import Path
9
+ import platform
10
+ import sys
11
+ from typing import List, Optional
12
+
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.table import Table
16
+ from rich.text import Text
17
+
18
+ from kapsel import __version__
19
+ from kapsel.core.detector import detector
20
+ from kapsel.i18n import _
21
+ from kapsel.storage.config import load_config
22
+ from kapsel.storage.logger import get_kapsel_dir
23
+ from kapsel.ui.banner import ensure_utf8_io
24
+
25
+
26
+ def handle_status(args: Optional[List[str]] = None, console: Optional[Console] = None) -> int:
27
+ """Renders the detailed Kapsel environment and runtime status dashboard."""
28
+ ensure_utf8_io()
29
+ con = console or Console(legacy_windows=False)
30
+
31
+ shell_name, shell_path = detector.detect_shell()
32
+ is_elevated, elevated_label = detector.is_elevated()
33
+ cwd_raw = Path.cwd()
34
+ cwd_fmt = detector.format_cwd(cwd_raw)
35
+ branch = detector.get_git_branch(cwd_raw)
36
+ cfg = load_config()
37
+ sandbox_dir = get_kapsel_dir()
38
+
39
+ # Status Grid
40
+ grid = Table.grid(expand=True, padding=(0, 2))
41
+ grid.add_column(style="bold #00f0ff", justify="right", width=18)
42
+ grid.add_column(style="#e4e4e7")
43
+ grid.add_column(style="bold #a855f7", justify="right", width=18)
44
+ grid.add_column(style="#e4e4e7")
45
+
46
+ admin_str = _("Admin")
47
+ priv_badge = (
48
+ f"[bold #10b981][{elevated_label}][/]"
49
+ if not is_elevated
50
+ else f"[bold #f59e0b][{elevated_label} ({admin_str})][/]"
51
+ )
52
+
53
+ none_str = _("None")
54
+ grid.add_row(
55
+ f"🖥️ {_('Platform:')}",
56
+ f"{platform.system()} {platform.release()} ({platform.machine()})",
57
+ f"🐚 {_('Host Shell:')}",
58
+ f"[bold #38bdf8]{shell_name}[/] [dim]({shell_path})[/]",
59
+ )
60
+
61
+ grid.add_row(
62
+ f"⚡ {_('Privilege:')}",
63
+ priv_badge,
64
+ f"🌿 {_('Git Branch:')}",
65
+ f"[bold #10b981]{branch}[/]" if branch else f"[dim]{none_str}[/]",
66
+ )
67
+
68
+ grid.add_row(
69
+ f"📂 {_('Working Dir:')}",
70
+ f"[dim]{cwd_fmt}[/]",
71
+ f"💊 {_('Kapsel Version:')}",
72
+ f"[bold #00f0ff]v{__version__}[/] (Python {sys.version.split()[0]})",
73
+ )
74
+
75
+ theme_name = cfg.theme.get("name", "cyber_dark") if isinstance(cfg.theme, dict) else str(cfg.theme)
76
+ border_status = _("On") if cfg.enable_card_border else _("Off")
77
+ grid.add_row(
78
+ f"📦 {_('Sandbox Dir:')}",
79
+ f"[dim]{sandbox_dir}[/]",
80
+ f"🎨 {_('Active Theme:')}",
81
+ f"[bold #a855f7]{theme_name}[/] [dim](Border: {border_status})[/]",
82
+ )
83
+
84
+ # Completion & Plugins info
85
+ from kapsel.completion.kps.registry import get_kps_registry
86
+ registry = get_kps_registry()
87
+ commands = registry.list_commands()
88
+
89
+ from kapsel.completion.carapace_engine import get_carapace_engine
90
+ carapace_eng = get_carapace_engine()
91
+ if carapace_eng.is_available():
92
+ tools_count = len(carapace_eng.get_supported_tools())
93
+ completer_label = f"[bold #10b981]{tools_count}+[/] Carapace specs"
94
+ else:
95
+ specs_dir = Path(__file__).resolve().parent.parent.parent / "specs"
96
+ spec_count = len(list(specs_dir.glob("*.json"))) if specs_dir.exists() else 0
97
+ completer_label = f"[dim]{spec_count} Fig specs[/]"
98
+
99
+ is_active = os.environ.get("KAPSEL_ACTIVE") == "1"
100
+ active_str = _("Active")
101
+ standby_str = _("Standby")
102
+ grid.add_row(
103
+ f"🎯 {_('Completer:')}",
104
+ completer_label,
105
+ f"⚙️ {_('Session Mode:')}",
106
+ f"[bold #10b981]🟢 {active_str}[/]" if is_active else f"[dim]⚪ {standby_str}[/]",
107
+ )
108
+
109
+ title_str = _("💊 KAPSEL System & Environment Status")
110
+ panel = Panel(
111
+ grid,
112
+ title=f"[bold #00f0ff]{title_str}[/]",
113
+ title_align="left",
114
+ border_style="#0891b2",
115
+ padding=(1, 2),
116
+ )
117
+
118
+ con.print()
119
+ con.print(panel)
120
+ con.print()
121
+ return 0
@@ -0,0 +1,34 @@
1
+ """
2
+ Kapsel Toggle Command: 'kapsel toggle'.
3
+ Toggles Kapsel as the default terminal environment.
4
+ Typing it once opens the interactive Kapsel capsule shell.
5
+ Typing it a second time closes the shell and returns to the host terminal.
6
+ All comments and descriptions are in English.
7
+ """
8
+
9
+ import os
10
+ from typing import List, Optional
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+
14
+ from kapsel.i18n import _
15
+ from kapsel.ui.banner import ensure_utf8_io
16
+
17
+
18
+ def handle_toggle_command(args: Optional[List[str]] = None, console: Optional[Console] = None) -> int:
19
+ """
20
+ Handles 'kapsel toggle' command.
21
+ Reports the toggle mechanism or current status.
22
+ """
23
+ ensure_utf8_io()
24
+ con = console or Console(legacy_windows=False)
25
+
26
+ is_active = os.environ.get("KAPSEL_ACTIVE") == "1"
27
+
28
+ if is_active:
29
+ msg = _("🔌 Kapsel session is active. Type 'kapsel toggle' or 'exit' to quit.")
30
+ con.print(f"[bold #00f0ff]{msg}[/]")
31
+ else:
32
+ msg = _("✔ Kapsel session is ready. Run 'kapsel toggle' to launch.")
33
+ con.print(f"[bold #10b981]{msg}[/]")
34
+ return 0
@@ -0,0 +1,65 @@
1
+ """
2
+ Kapsel Command Dispatcher.
3
+ Dispatches 'kapsel <cmd>' and 'kps <cmd>' uniformly.
4
+ Unified architecture ensures both prefixes share the exact same command pipeline.
5
+ """
6
+
7
+ from typing import List, Optional
8
+ from rich.console import Console
9
+
10
+ from kapsel.completion.kps.registry import get_kps_registry
11
+ from kapsel.i18n import _
12
+
13
+
14
+ def dispatch_kps(command_line: str, console: Optional[Console] = None) -> Optional[int]:
15
+ """
16
+ Dispatches command lines starting with 'kapsel' or 'kps'.
17
+ Both 'kapsel <cmd>' and 'kps <cmd>' route to the unified command registry.
18
+ Returns exit code (int) if handled, or None if not recognized or not a kapsel/kps command.
19
+ """
20
+ stripped = command_line.strip()
21
+ if not stripped:
22
+ return 0
23
+
24
+ con = console or Console(legacy_windows=False)
25
+ registry = get_kps_registry()
26
+
27
+ # Determine command prefix and subcommand string
28
+ if stripped.startswith("kapsel "):
29
+ sub = stripped[7:].strip()
30
+ elif stripped == "kapsel":
31
+ sub = ""
32
+ elif stripped.startswith("kps "):
33
+ sub = stripped[4:].strip()
34
+ elif stripped == "kps":
35
+ sub = ""
36
+ else:
37
+ # Not a kapsel or kps command: do not intercept
38
+ return None
39
+
40
+ # Handle bare 'kapsel' or 'kps' -> Show comprehensive manual and commands
41
+ if not sub:
42
+ help_cmd = registry.get("help")
43
+ if help_cmd:
44
+ return help_cmd.handler([], con)
45
+ return 0
46
+
47
+ parts = sub.split()
48
+ cmd_name = parts[0].lower()
49
+ args = parts[1:]
50
+
51
+ # Check alias shortcuts
52
+ if cmd_name in ("?", "-h", "--help"):
53
+ cmd_name = "help"
54
+ elif cmd_name == "info":
55
+ cmd_name = "status"
56
+
57
+ # Route to unified command registry
58
+ cmd = registry.get(cmd_name)
59
+ if cmd:
60
+ return cmd.handler(args, con)
61
+
62
+ msg = _("kapsel: unknown command '{cmd}'. See 'kapsel help'.").format(cmd=cmd_name)
63
+ con.print(f"[bold #f43f5e]{msg}[/]")
64
+ return 1
65
+
@@ -0,0 +1,101 @@
1
+ """
2
+ Kapsel Command Registry.
3
+ Unified Single Source of Truth for system management ('kapsel <cmd>')
4
+ and feature extension ('kps <cmd>') commands.
5
+ Used by autocompletion engines and command line dispatchers.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Callable, Dict, List, Optional
10
+ from rich.console import Console
11
+
12
+
13
+ @dataclass
14
+ class KpsCommand:
15
+ """
16
+ Represents a registered command in the Kapsel ecosystem.
17
+ Shared uniformly across 'kapsel <cmd>' and 'kps <cmd>'.
18
+ """
19
+ name: str
20
+ help_text: str
21
+ handler: Callable[[List[str], Optional[Console]], Optional[int]]
22
+ subcommands: Dict[str, str] = field(default_factory=dict)
23
+ usage: Optional[str] = None
24
+ plugin_id: Optional[str] = None
25
+ scope: str = "default"
26
+
27
+
28
+ class KpsCommandRegistry:
29
+ """Central registry storing all Kapsel commands uniformly."""
30
+
31
+ def __init__(self):
32
+ # Keyed by command name (lowercase) to provide a unified command pipeline
33
+ self._commands: Dict[str, KpsCommand] = {}
34
+
35
+ def register(
36
+ self,
37
+ name: str,
38
+ handler: Callable[[List[str], Optional[Console]], Optional[int]],
39
+ help_text: str,
40
+ subcommands: Optional[Dict[str, str]] = None,
41
+ usage: Optional[str] = None,
42
+ plugin_id: Optional[str] = None,
43
+ scope: str = "default",
44
+ ) -> KpsCommand:
45
+ """Registers a command into the unified registry."""
46
+ clean_name = name.lower().strip()
47
+ cmd = KpsCommand(
48
+ name=clean_name,
49
+ help_text=help_text,
50
+ handler=handler,
51
+ subcommands=subcommands or {},
52
+ usage=usage,
53
+ plugin_id=plugin_id,
54
+ scope=scope,
55
+ )
56
+ self._commands[clean_name] = cmd
57
+ return cmd
58
+
59
+ def get(self, name: str, scope: Optional[str] = None) -> Optional[KpsCommand]:
60
+ """Retrieves a command by name from the unified registry."""
61
+ clean_name = name.lower().strip()
62
+ return self._commands.get(clean_name)
63
+
64
+ def list_commands(self) -> List[KpsCommand]:
65
+ """Returns all registered commands sorted by name."""
66
+ return sorted(self._commands.values(), key=lambda c: c.name)
67
+
68
+ def list_system_commands(self) -> List[KpsCommand]:
69
+ """Backward-compatibility alias returning all core built-in commands."""
70
+ return sorted(
71
+ [c for c in self._commands.values() if not c.plugin_id],
72
+ key=lambda c: c.name,
73
+ )
74
+
75
+ def list_feature_commands(self) -> List[KpsCommand]:
76
+ """Backward-compatibility alias returning all plugin-provided commands."""
77
+ return sorted(
78
+ [c for c in self._commands.values() if c.plugin_id],
79
+ key=lambda c: c.name,
80
+ )
81
+
82
+ def remove_by_plugin(self, plugin_id: str) -> None:
83
+ """Removes all commands registered by a specific plugin."""
84
+ self._commands = {
85
+ k: v for k, v in self._commands.items() if v.plugin_id != plugin_id
86
+ }
87
+
88
+
89
+ # Global registry singleton
90
+ _GLOBAL_REGISTRY: Optional[KpsCommandRegistry] = None
91
+
92
+
93
+ def get_kps_registry() -> KpsCommandRegistry:
94
+ """Gets or initializes the global command registry."""
95
+ global _GLOBAL_REGISTRY
96
+ if _GLOBAL_REGISTRY is None:
97
+ _GLOBAL_REGISTRY = KpsCommandRegistry()
98
+ # Auto-register core built-in commands
99
+ from kapsel.completion.kps.builtins import register_builtins
100
+ register_builtins(_GLOBAL_REGISTRY)
101
+ return _GLOBAL_REGISTRY
@@ -0,0 +1,28 @@
1
+ """
2
+ Kapsel core engine package.
3
+ """
4
+
5
+ from kapsel.core.detector import EnvironmentDetector, detector
6
+ from kapsel.core.engine import DispatchResult, DualStateEngine
7
+ from kapsel.core.executor import CommandExecutor, ExecutionSummary
8
+ from kapsel.core.plugin import (
9
+ HookType,
10
+ KapselPlugin,
11
+ PluginContext,
12
+ PluginManager,
13
+ PluginManifest,
14
+ )
15
+
16
+ __all__ = [
17
+ "EnvironmentDetector",
18
+ "detector",
19
+ "CommandExecutor",
20
+ "ExecutionSummary",
21
+ "DualStateEngine",
22
+ "DispatchResult",
23
+ "KapselPlugin",
24
+ "PluginManifest",
25
+ "PluginContext",
26
+ "HookType",
27
+ "PluginManager",
28
+ ]
@@ -0,0 +1,162 @@
1
+ """
2
+ Kapsel environment and shell sniffing detector.
3
+ Identifies host shell, elevation level, current working directory, and Git branch.
4
+ """
5
+
6
+ import ctypes
7
+ import os
8
+ from pathlib import Path
9
+ import platform
10
+ import sys
11
+ from typing import Optional, Tuple
12
+
13
+ import psutil
14
+ import shellingham
15
+
16
+ from kapsel.storage.logger import logger
17
+
18
+
19
+ class EnvironmentDetector:
20
+ """Sniffs host shell, user privileges, Git state, and directory paths."""
21
+
22
+ KNOWN_SHELLS = {
23
+ "pwsh": "pwsh",
24
+ "pwsh.exe": "pwsh",
25
+ "powershell": "powershell",
26
+ "powershell.exe": "powershell",
27
+ "cmd": "cmd",
28
+ "cmd.exe": "cmd",
29
+ "bash": "bash",
30
+ "bash.exe": "bash",
31
+ "zsh": "zsh",
32
+ "fish": "fish",
33
+ "sh": "sh",
34
+ }
35
+
36
+ def __init__(self):
37
+ self._cached_shell: Optional[str] = None
38
+ self._cached_shell_path: Optional[str] = None
39
+
40
+ def detect_shell(self) -> Tuple[str, str]:
41
+ """
42
+ Detects the outer host shell name and executable path.
43
+ Returns (shell_name, executable_path), e.g. ('pwsh', 'C:\\...\\pwsh.exe').
44
+ """
45
+ if self._cached_shell and self._cached_shell_path:
46
+ return self._cached_shell, self._cached_shell_path
47
+
48
+ # 1. Try psutil process tree lookup (inspect parents of current process)
49
+ try:
50
+ current_proc = psutil.Process(os.getpid())
51
+ for parent in current_proc.parents():
52
+ name = parent.name().lower()
53
+ if name in self.KNOWN_SHELLS:
54
+ canonical = self.KNOWN_SHELLS[name]
55
+ try:
56
+ exe = parent.exe()
57
+ except Exception:
58
+ exe = name
59
+ self._cached_shell = canonical
60
+ self._cached_shell_path = exe
61
+ logger.debug(f"Detected host shell from parent process: {canonical} ({exe})")
62
+ return canonical, exe
63
+ except Exception as e:
64
+ logger.debug(f"Process tree shell detection failed: {e}")
65
+
66
+ # 2. Try shellingham
67
+ try:
68
+ detected = shellingham.detect_shell()
69
+ if detected:
70
+ raw_name, raw_path = detected
71
+ canonical = self.KNOWN_SHELLS.get(raw_name.lower(), raw_name.lower())
72
+ self._cached_shell = canonical
73
+ self._cached_shell_path = raw_path
74
+ logger.debug(f"Detected host shell from shellingham: {canonical} ({raw_path})")
75
+ return canonical, raw_path
76
+ except Exception as e:
77
+ logger.debug(f"Shellingham detection failed: {e}")
78
+
79
+ # 3. Environment variable fallback
80
+ if platform.system() == "Windows":
81
+ comspec = os.environ.get("COMSPEC", "cmd.exe")
82
+ # If PSModulePath is set and contains pwsh
83
+ ps_module_path = os.environ.get("PSModulePath", "")
84
+ if "PowerShell\\7" in ps_module_path or "pwsh" in ps_module_path.lower():
85
+ self._cached_shell = "pwsh"
86
+ self._cached_shell_path = "pwsh.exe"
87
+ elif "WindowsPowerShell" in ps_module_path:
88
+ self._cached_shell = "powershell"
89
+ self._cached_shell_path = "powershell.exe"
90
+ else:
91
+ self._cached_shell = "cmd"
92
+ self._cached_shell_path = comspec
93
+ else:
94
+ shell_env = os.environ.get("SHELL", "/bin/sh")
95
+ base_name = Path(shell_env).name.lower()
96
+ self._cached_shell = self.KNOWN_SHELLS.get(base_name, base_name)
97
+ self._cached_shell_path = shell_env
98
+
99
+ logger.debug(f"Fallback shell detection: {self._cached_shell} ({self._cached_shell_path})")
100
+ return self._cached_shell, self._cached_shell_path
101
+
102
+ def is_elevated(self) -> Tuple[bool, str]:
103
+ """
104
+ Check if running with elevated privileges (Admin on Windows, Root on Unix).
105
+ Returns (is_elevated, label), e.g. (True, "Admin") or (False, "User").
106
+ """
107
+ try:
108
+ if platform.system() == "Windows":
109
+ is_admin = bool(ctypes.windll.shell32.IsUserAnAdmin())
110
+ return is_admin, "Admin" if is_admin else "User"
111
+ else:
112
+ is_root = (os.geteuid() == 0)
113
+ return is_root, "Root" if is_root else "User"
114
+ except Exception as e:
115
+ logger.debug(f"Elevation detection failed: {e}")
116
+ return False, "User"
117
+
118
+ def get_git_branch(self, path: Optional[Path] = None) -> Optional[str]:
119
+ """
120
+ Quickly inspects .git/HEAD in current or ancestor directories to extract the active branch.
121
+ Does not spawn external git processes, ensuring near-instant performance.
122
+ """
123
+ target = (path or Path.cwd()).resolve()
124
+ for directory in [target] + list(target.parents):
125
+ git_dir = directory / ".git"
126
+ if git_dir.is_file():
127
+ # Git worktree / submodule pointer
128
+ try:
129
+ content = git_dir.read_text(encoding="utf-8").strip()
130
+ if content.startswith("gitdir:"):
131
+ ref_path = (directory / content.split(":", 1)[1].strip()).resolve()
132
+ git_dir = ref_path
133
+ except Exception:
134
+ pass
135
+
136
+ if git_dir.is_dir():
137
+ head_file = git_dir / "HEAD"
138
+ if head_file.exists():
139
+ try:
140
+ content = head_file.read_text(encoding="utf-8").strip()
141
+ if content.startswith("ref: refs/heads/"):
142
+ return content.replace("ref: refs/heads/", "").strip()
143
+ elif content:
144
+ return content[:7] # Detached commit hash
145
+ except Exception:
146
+ pass
147
+ return None
148
+
149
+ def format_cwd(self, path: Optional[Path] = None) -> str:
150
+ """
151
+ Formats current working directory, abbreviating home directory to '~'.
152
+ """
153
+ p = (path or Path.cwd()).resolve()
154
+ home = Path.home().resolve()
155
+ try:
156
+ rel = p.relative_to(home)
157
+ return f"~/{rel.as_posix()}" if str(rel) != "." else "~"
158
+ except ValueError:
159
+ return str(p)
160
+
161
+
162
+ detector = EnvironmentDetector()
kapsel/core/engine.py ADDED
@@ -0,0 +1,159 @@
1
+ """
2
+ Kapsel Core Engine orchestrator.
3
+ Manages state transitions, plugin lifecycle, command dispatching, and execution history.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ import time
9
+ from typing import Any, Optional, Tuple
10
+
11
+ from kapsel.completion.kps.dispatcher import dispatch_kps
12
+ from kapsel.completion.kps.registry import KpsCommandRegistry, get_kps_registry
13
+ from kapsel.core.detector import EnvironmentDetector, detector
14
+ from kapsel.core.executor import CommandExecutor, ExecutionSummary
15
+ from kapsel.core.plugin.hooks import HookType
16
+ from kapsel.i18n import _
17
+ from kapsel.core.plugin.manager import PluginManager
18
+ from kapsel.storage.config import KapselConfig, load_config
19
+ from kapsel.storage.history import HistoryManager
20
+ from kapsel.storage.logger import logger
21
+
22
+
23
+ @dataclass
24
+ class DispatchResult:
25
+ mode: str # "kps" or "native"
26
+ raw_input: str
27
+ translated_cmd: Optional[str]
28
+ execution: ExecutionSummary
29
+
30
+
31
+ class DualStateEngine:
32
+ """The central brain of Kapsel orchestrating dual-state execution, plugins, and state persistence."""
33
+
34
+ def __init__(
35
+ self,
36
+ config: Optional[KapselConfig] = None,
37
+ kps_registry: Optional[KpsCommandRegistry] = None,
38
+ history_mgr: Optional[HistoryManager] = None,
39
+ env_detector: Optional[EnvironmentDetector] = None,
40
+ ):
41
+ self.config = config or load_config()
42
+ self.history_mgr = history_mgr or HistoryManager()
43
+ self.detector = env_detector or detector
44
+ self.kps_registry = kps_registry or get_kps_registry()
45
+
46
+ # Detect host shell
47
+ self.shell_name, self.shell_path = self.detector.detect_shell()
48
+ self.is_elevated, self.elevated_label = self.detector.is_elevated()
49
+
50
+ # Initialize executor
51
+ self.executor = CommandExecutor(self.shell_name, self.shell_path)
52
+
53
+ # Initialize Plugin Subsystem
54
+ self.plugin_manager = PluginManager(
55
+ config=self.config,
56
+ kps_command_register_fn=self.kps_registry.register,
57
+ env_detector=self.detector,
58
+ )
59
+ self.plugin_manager.load_all_plugins()
60
+ self.plugin_manager.trigger_hook(HookType.ON_READY)
61
+
62
+ def reload(self) -> None:
63
+ """Reload configuration and refresh plugins."""
64
+ self.config = load_config()
65
+ self.plugin_manager.unload_all()
66
+ self.plugin_manager.load_all_plugins()
67
+
68
+ def is_kapsel_mode(self, text: str) -> bool:
69
+ """Determines if the current text line represents Kapsel Command Mode."""
70
+ stripped = text.strip()
71
+ return (
72
+ stripped in ("kapsel", "kps")
73
+ or stripped.startswith("kapsel ")
74
+ or stripped.startswith("kps ")
75
+ )
76
+
77
+ is_kps_mode = is_kapsel_mode
78
+
79
+ def dispatch(self, user_input: str) -> DispatchResult:
80
+ """
81
+ Dispatches input line either to Plugin Filter, Kapsel Builtin, or Native execution.
82
+ Records execution duration, status, and history in SQLite history.db.
83
+ """
84
+ stripped = user_input.strip()
85
+ cwd_str = str(Path.cwd())
86
+ start_time = time.time()
87
+
88
+ # 1. Plugin Pre-execution filter (e.g. mapping plugin intercepts and translates)
89
+ is_filtered, transformed_cmd = self.plugin_manager.filter_command(user_input)
90
+
91
+ if is_filtered:
92
+ mode = "kapsel"
93
+ cmd_to_run = transformed_cmd
94
+ exec_summary = self.executor.execute(cmd_to_run)
95
+ translated_cmd = cmd_to_run
96
+
97
+ elif self.is_kapsel_mode(user_input):
98
+ mode = "kapsel"
99
+ translated_cmd = None
100
+
101
+ # Try dispatching to core/plugin kps/kapsel subcommands
102
+ t0 = time.perf_counter()
103
+ handled_code = dispatch_kps(user_input)
104
+ t1 = time.perf_counter()
105
+ duration_ms = int((t1 - t0) * 1000)
106
+
107
+ if handled_code is not None:
108
+ exec_summary = ExecutionSummary(
109
+ command=user_input,
110
+ exit_code=handled_code,
111
+ duration_ms=duration_ms,
112
+ duration_str=f"{duration_ms}ms",
113
+ success=(handled_code == 0),
114
+ is_builtin=True,
115
+ )
116
+ else:
117
+ # Not a recognized builtin command
118
+ msg = _("kapsel: unknown command '{cmd}'. See 'kapsel help'.").format(cmd=user_input)
119
+ print(msg, file=sys.stderr)
120
+ exec_summary = ExecutionSummary(
121
+ command=user_input,
122
+ exit_code=1,
123
+ duration_ms=0,
124
+ duration_str="0ms",
125
+ success=False,
126
+ is_builtin=True,
127
+ )
128
+
129
+ else:
130
+ mode = "native"
131
+ translated_cmd = None
132
+ exec_summary = self.executor.execute(user_input)
133
+
134
+ # Record to history database
135
+ self.history_mgr.add_record(
136
+ command=user_input,
137
+ translated_cmd=translated_cmd,
138
+ mode=mode,
139
+ cwd=cwd_str,
140
+ shell=self.shell_name,
141
+ timestamp=start_time,
142
+ duration_ms=exec_summary.duration_ms,
143
+ exit_code=exec_summary.exit_code,
144
+ )
145
+
146
+ # Trigger plugin post-execution hook
147
+ self.plugin_manager.trigger_hook(
148
+ HookType.ON_AFTER_EXECUTE,
149
+ command=user_input,
150
+ exit_code=exec_summary.exit_code,
151
+ duration_ms=exec_summary.duration_ms,
152
+ )
153
+
154
+ return DispatchResult(
155
+ mode=mode,
156
+ raw_input=user_input,
157
+ translated_cmd=translated_cmd,
158
+ execution=exec_summary,
159
+ )