cct-cli 0.7.9.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 (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1024 @@
1
+ """
2
+ CCT — packages.py: Autonomous Package Manager (v0.7.7 spec section 1/10/11/12).
3
+
4
+ Lets the user (or the AI, in Build mode) install real packages from chat:
5
+
6
+ "install requests" -> pip
7
+ "install numpy" -> pip
8
+ "install latest opencv" -> pip, pinned to the latest release
9
+ "install nodejs" -> system package manager (winget/choco/brew/apt/dnf)
10
+ "install rust" -> rustup (if present) or the system package manager
11
+ "install docker" -> system package manager
12
+ "install cuda" -> system package manager
13
+ "install lodash with npm" -> npm (explicit manager hint honored)
14
+ "/install torch --manager pipx"
15
+
16
+ Every supported package manager (pip, pipx, uv, conda, npm, pnpm, yarn,
17
+ bun, cargo, go, composer, vcpkg, chocolatey, winget, apt, dnf, brew) maps
18
+ to a ManagerSpec with real command builders and a best-effort verify step.
19
+
20
+ Honest scope, stated plainly:
21
+ * "Install" means: run the package manager's real install command in a
22
+ subprocess on the user's machine. It never happens silently — the
23
+ permission gate lives in the callers (calc_terminal/app.py,
24
+ calc_terminal/ui/app.py, calc_terminal/agent.py), and every stage of
25
+ the run is reported through an `on_event` callback so the UI can
26
+ render the Installation Dashboard (spec section 11).
27
+ * Manager detection is heuristic (keyword + platform tables), not a
28
+ semantic package database. When a package is genuinely ambiguous or
29
+ unknown, the caller is expected to run the research flow
30
+ (package_research.py) BEFORE installing — this module only decides
31
+ on the *known* mappings and says "unknown" honestly otherwise.
32
+ * Real download/install progress bars from e.g. pip/npm live on the
33
+ child process's stdout/stderr; this module streams those lines
34
+ through verbatim rather than fabricating percentages.
35
+ * sudo-requiring managers (apt/dnf) build the `sudo ...` command and
36
+ pass the user's permission dialog through first; the sudo password
37
+ prompt itself happens in the user's real terminal when the command
38
+ runs, never captured here.
39
+ """
40
+
41
+ if __name__ == "__main__":
42
+ print("This is a library file and is not meant to be run directly.")
43
+ import sys
44
+ sys.exit(1)
45
+
46
+ import json
47
+ import os
48
+ import re
49
+ import shutil
50
+ import subprocess
51
+ import sys
52
+ import time
53
+
54
+ from . import eventbus
55
+
56
+ # ------------------------------------------------------------------ events --
57
+ # Installation Dashboard event vocabulary. Every event is a plain dict:
58
+ # {"stage": <one of the keys below>, "message": str, "detail": optional str}
59
+ # Callers render these; this module never prints them itself (the classic
60
+ # terminal and the Textual UI each have their own renderer).
61
+ STAGE_RESOLVING = "resolving"
62
+ STAGE_PERMISSION = "permission"
63
+ STAGE_DOWNLOAD = "download"
64
+ STAGE_RUNNING = "running"
65
+ STAGE_VERIFYING = "verifying"
66
+ STAGE_DONE = "done"
67
+ STAGE_FAILED = "failed"
68
+ STAGE_SKIPPED = "skipped"
69
+
70
+ # ---------------------------------------------------------------- managers --
71
+ # name -> (label, install-cmd builder, verify hint). `platforms` is the
72
+ # OS list the manager is native to ("" = any). The install builders take
73
+ # (package_name, version) and return the argv (list) to run — lists, not
74
+ # shell strings, so no quoting surprises reach a shell.
75
+
76
+ _WIN = os.name == "nt"
77
+ _MAC = sys.platform == "darwin"
78
+ _LINUX = sys.platform.startswith("linux")
79
+
80
+
81
+ def _py():
82
+ """The python interpreter this app is running on (used for pip
83
+ invocation, so 'install requests' installs into the environment
84
+ CCT itself runs in)."""
85
+ return sys.executable or "python"
86
+
87
+
88
+ def _pip_install(name, version, target=None):
89
+ cmd = [_py(), "-m", "pip", "install", "--no-input"]
90
+ if version and version != "latest":
91
+ cmd.append(f"{name}=={version}")
92
+ else:
93
+ cmd.append(name)
94
+ return cmd
95
+
96
+
97
+ def _npm_install(name, version, target=None):
98
+ cmd = ["npm", "install", "-g", name] if target != "local" else ["npm", "install", name]
99
+ return cmd
100
+
101
+
102
+ def _apt_install(name, version, target=None):
103
+ return ["sudo", "apt-get", "install", "-y", name]
104
+
105
+
106
+ def _dnf_install(name, version, target=None):
107
+ return ["sudo", "dnf", "install", "-y", name]
108
+
109
+
110
+ MANAGERS = {
111
+ "pip": dict(
112
+ label="pip (Python)",
113
+ icon="\U0001f40d",
114
+ platforms="",
115
+ install=_pip_install,
116
+ verify=lambda name, version: [_py(), "-m", "pip", "show", name],
117
+ check_import=True,
118
+ ),
119
+ "pipx": dict(
120
+ label="pipx (Python apps)",
121
+ icon="\U0001f40d",
122
+ platforms="",
123
+ install=lambda name, version, target=None: ["pipx", "install", name],
124
+ verify=lambda name, version: ["pipx", "list"],
125
+ check_import=False,
126
+ ),
127
+ "uv": dict(
128
+ label="uv (Rust-based pip)",
129
+ icon="\u26a1",
130
+ platforms="",
131
+ install=lambda name, version, target=None: ["uv", "pip", "install", name],
132
+ verify=lambda name, version: ["uv", "pip", "show", name],
133
+ check_import=True,
134
+ ),
135
+ "conda": dict(
136
+ label="conda (Anaconda)",
137
+ icon="\U0001f331",
138
+ platforms="",
139
+ install=lambda name, version, target=None: ["conda", "install", "-y", name],
140
+ verify=lambda name, version: ["conda", "list", name],
141
+ check_import=True,
142
+ ),
143
+ "npm": dict(
144
+ label="npm (Node.js)",
145
+ icon="\U0001f4e6",
146
+ platforms="",
147
+ install=_npm_install,
148
+ verify=lambda name, version: ["npm", "ls", "-g", name],
149
+ check_import=False,
150
+ ),
151
+ "pnpm": dict(
152
+ label="pnpm (Node.js)",
153
+ icon="\U0001f4e6",
154
+ platforms="",
155
+ install=lambda name, version, target=None: (
156
+ ["pnpm", "add", "-g", name] if target != "local" else ["pnpm", "add", name]),
157
+ verify=lambda name, version: ["pnpm", "ls", "-g", name],
158
+ check_import=False,
159
+ ),
160
+ "yarn": dict(
161
+ label="yarn (Node.js)",
162
+ icon="\U0001f4e6",
163
+ platforms="",
164
+ install=lambda name, version, target=None: (
165
+ ["yarn", "global", "add", name] if target != "local" else ["yarn", "add", name]),
166
+ verify=lambda name, version: ["yarn", "global", "list"],
167
+ check_import=False,
168
+ ),
169
+ "bun": dict(
170
+ label="bun (Node.js runtime)",
171
+ icon="\U0001f35c",
172
+ platforms="",
173
+ install=lambda name, version, target=None: (
174
+ ["bun", "add", "-g", name] if target != "local" else ["bun", "add", name]),
175
+ verify=lambda name, version: ["bun", "pm", "ls", "-g"],
176
+ check_import=False,
177
+ ),
178
+ "cargo": dict(
179
+ label="cargo (Rust crates)",
180
+ icon="\U0001f680",
181
+ platforms="",
182
+ install=lambda name, version, target=None: ["cargo", "install", name],
183
+ verify=lambda name, version: ["cargo", "install", "--list"],
184
+ check_import=False,
185
+ ),
186
+ "go": dict(
187
+ label="go (Go modules)",
188
+ icon="\U0001f4a0",
189
+ platforms="",
190
+ install=lambda name, version, target=None: ["go", "install", f"{name}@latest"],
191
+ verify=lambda name, version: ["go", "list", "-m", name],
192
+ check_import=False,
193
+ ),
194
+ "composer": dict(
195
+ label="composer (PHP)",
196
+ icon="\U0001f4e6",
197
+ platforms="",
198
+ install=lambda name, version, target=None: ["composer", "global", "require", name],
199
+ verify=lambda name, version: ["composer", "global", "show", name],
200
+ check_import=False,
201
+ ),
202
+ "vcpkg": dict(
203
+ label="vcpkg (C/C++)",
204
+ icon="\U0001f6e0\ufe0f",
205
+ platforms="",
206
+ install=lambda name, version, target=None: ["vcpkg", "install", name],
207
+ verify=lambda name, version: ["vcpkg", "list", name],
208
+ check_import=False,
209
+ ),
210
+ "chocolatey": dict(
211
+ label="chocolatey (Windows)",
212
+ icon="\U0001f36b",
213
+ platforms="win",
214
+ install=lambda name, version, target=None: ["choco", "install", "-y", name],
215
+ verify=lambda name, version: ["choco", "list", name, "--local-only"],
216
+ check_import=False,
217
+ ),
218
+ "winget": dict(
219
+ label="winget (Windows)",
220
+ icon="\U0001f310",
221
+ platforms="win",
222
+ install=lambda name, version, target=None: [
223
+ "winget", "install", "--silent",
224
+ "--accept-package-agreements", "--accept-source-agreements", name],
225
+ verify=lambda name, version: ["winget", "list", name],
226
+ check_import=False,
227
+ ),
228
+ "apt": dict(
229
+ label="apt (Debian/Ubuntu)",
230
+ icon="\U0001f3e2",
231
+ platforms="linux",
232
+ install=_apt_install,
233
+ verify=lambda name, version: ["apt", "list", "--installed", name + "*"],
234
+ check_import=False,
235
+ ),
236
+ "dnf": dict(
237
+ label="dnf (Fedora/RHEL)",
238
+ icon="\U0001f3e2",
239
+ platforms="linux",
240
+ install=_dnf_install,
241
+ verify=lambda name, version: ["dnf", "list", "--installed", name],
242
+ check_import=False,
243
+ ),
244
+ "brew": dict(
245
+ label="homebrew (macOS/Linux)",
246
+ icon="\U0001f37a",
247
+ platforms="darwin",
248
+ install=lambda name, version, target=None: ["brew", "install", name],
249
+ verify=lambda name, version: ["brew", "list", name],
250
+ check_import=False,
251
+ ),
252
+ }
253
+
254
+ # Python package -> importable module name (for the import-succeeds
255
+ # verification step of spec section 12). Only the common renames; a
256
+ # package whose import name matches its install name uses itself.
257
+ _IMPORT_ALIASES = {
258
+ "opencv-python": "cv2",
259
+ "opencv-python-headless": "cv2",
260
+ "Pillow": "PIL",
261
+ "PyYAML": "yaml",
262
+ "beautifulsoup4": "bs4",
263
+ "scikit-learn": "sklearn",
264
+ "scikit-image": "skimage",
265
+ "python-dateutil": "dateutil",
266
+ "requests-html": "requests_html",
267
+ "psycopg2-binary": "psycopg2",
268
+ "pytz": "pytz",
269
+ "numpy": "numpy",
270
+ "torch": "torch",
271
+ "tensorflow": "tensorflow",
272
+ "matplotlib": "matplotlib",
273
+ "sympy": "sympy",
274
+ "pandas": "pandas",
275
+ }
276
+
277
+ # Known-language package tables (for manager detection). These are the
278
+ # honest heuristics behind "install requests" -> pip: a curated list of
279
+ # well-known packages per ecosystem, not a live index.
280
+ PYTHON_PACKAGES = {
281
+ "requests", "numpy", "pandas", "scipy", "sympy", "matplotlib", "seaborn",
282
+ "scikit-learn", "scikit-image", "torch", "tensorflow", "keras", "jax",
283
+ "transformers", "torchvision", "torchaudio", "opencv-python",
284
+ "opencv-python-headless", "pillow", "pyyaml", "flask", "django",
285
+ "fastapi", "uvicorn", "pytest", "black", "ruff", "isort", "mypy",
286
+ "beautifulsoup4", "requests-html", "selenium", "playwright", "httpx",
287
+ "aiohttp", "websockets", "tqdm", "click", "typer", "rich", "textual",
288
+ "prompt-toolkit", "colorama", "boto3", "gunicorn", "celery", "redis",
289
+ "pymongo", "psycopg2-binary", "sqlalchemy", "alembic", "pydantic",
290
+ "pydantic-settings", "pyqt5", "pyside6", "tkinter", "jupyter",
291
+ "notebook", "ipykernel", "ipython", "nltk", "spacy", "gensim",
292
+ "xgboost", "lightgbm", "statsmodels", "polars", "pyarrow", "dask",
293
+ "joblib", "bokeh", "plotly", "kaleido", "networkx", "biopython",
294
+ "rdkit", "pubchempy", "ase", "pymatgen", "openpyxl", "xlrd", "xlsxwriter",
295
+ "pdfplumber", "pypdf", "fitz", "pyinstaller", "pip", "pipx", "uv",
296
+ }
297
+ NODE_PACKAGES = {
298
+ "lodash", "express", "react", "react-dom", "vue", "next", "nuxt", "svelte",
299
+ "typescript", "ts-node", "nodemon", "axios", "node-fetch", "moment",
300
+ "chalk", "commander", "yargs", "dotenv", "cors", "body-parser",
301
+ "socket.io", "ws", "jquery", "eslint", "prettier", "jest", "mocha",
302
+ "vitest", "vite", "webpack", "babel", "prisma", "mongoose", "pg",
303
+ "mysql2", "sequelize", "knex", "graphql", "apollo-server", "next-auth",
304
+ "tailwindcss", "zustand", "redux", "react-router-dom", "uuid",
305
+ "express-session", "passport", "bcrypt", "jsonwebtoken", "zod",
306
+ "yup", "formik", "framer-motion", "three", "d3", "chart.js",
307
+ "electron", "expo", "react-native", "firebase", "stripe",
308
+ }
309
+ RUST_CRATES = {
310
+ "serde", "tokio", "rayon", "clap", "anyhow", "thiserror", "reqwest",
311
+ "hyper", "axum", "actix-web", "rocket", "tokio-postgres", "sqlx",
312
+ "diesel", "rusqlite", "rand", "chrono", "regex", "lazy_static",
313
+ "once_cell", "log", "env_logger", "tracing", "serde_json", "tokio-util",
314
+ "futures", "async-std", "tempfile", "walkdir", "glob", "dirs",
315
+ "csv", "serde_yaml", "toml", "jsonwebtoken", "rpassword", "indicatif",
316
+ "crossterm", "ratatui", "tui", "egui", "iced", "bevy", "nalgebra",
317
+ "ndarray", "num", "itertools", "uuid", "indexmap", "hashbrown",
318
+ "parking_lot", "dashmap", "crossbeam", "flume", "crossbeam-channel",
319
+ }
320
+ GO_MODULES = {
321
+ "gin", "echo", "chi", "mux", "gorilla/mux", "cobra", "viper", "zap",
322
+ "logrus", "gorm", "sqlx", "pgx", "fiber", "fasthttp", "grpc",
323
+ "protobuf", "uuid", "jwt", "casbin", "hugo", "docker", "go-git",
324
+ }
325
+ PHP_PACKAGES = {
326
+ "laravel/framework", "symfony/console", "monolog/monolog", "guzzlehttp/guzzle",
327
+ "phpunit/phpunit", "ramsey/uuid", "vlucas/phpdotenv", "phpoffice/phpspreadsheet",
328
+ "composer/semver", "nesbot/carbon",
329
+ }
330
+
331
+ # Well-known standalone tools -> (manager candidates in order, verify binary)
332
+ SYSTEM_TOOLS = {
333
+ "nodejs": (["winget", "chocolatey", "brew", "apt", "dnf"], "node"),
334
+ "node": (["winget", "chocolatey", "brew", "apt", "dnf"], "node"),
335
+ "npm": (["winget", "chocolatey", "brew", "apt", "dnf"], "npm"),
336
+ "rust": (["rustup"], "rustc"),
337
+ "rustup": (["winget", "chocolatey", "brew", "apt", "dnf"], "rustup"),
338
+ "rustc": (["rustup"], "rustc"),
339
+ "cargo": (["rustup"], "cargo"),
340
+ "docker": (["winget", "chocolatey", "brew", "apt", "dnf"], "docker"),
341
+ "docker desktop": (["winget", "chocolatey", "brew"], "docker"),
342
+ "cuda": (["winget", "chocolatey", "brew"], "nvcc"),
343
+ "cuda toolkit": (["winget", "chocolatey"], "nvcc"),
344
+ "go": (["winget", "chocolatey", "brew", "apt", "dnf"], "go"),
345
+ "golang": (["winget", "chocolatey", "brew", "apt", "dnf"], "go"),
346
+ "java": (["winget", "chocolatey", "brew", "apt", "dnf"], "java"),
347
+ "jdk": (["winget", "chocolatey", "brew", "apt", "dnf"], "java"),
348
+ "python": (["winget", "chocolatey", "brew", "apt", "dnf"], "python"),
349
+ "ffmpeg": (["winget", "chocolatey", "brew", "apt", "dnf"], "ffmpeg"),
350
+ "git": (["winget", "chocolatey", "brew", "apt", "dnf"], "git"),
351
+ "curl": (["winget", "chocolatey", "brew", "apt", "dnf"], "curl"),
352
+ "wget": (["winget", "chocolatey", "brew", "apt", "dnf"], "wget"),
353
+ "vim": (["winget", "chocolatey", "brew", "apt", "dnf"], "vim"),
354
+ "neovim": (["winget", "chocolatey", "brew", "apt", "dnf"], "nvim"),
355
+ "vscode": (["winget", "chocolatey", "brew"], "code"),
356
+ "git-lfs": (["winget", "chocolatey", "brew", "apt", "dnf"], "git-lfs"),
357
+ "kubernetes": (["winget", "chocolatey", "brew", "apt", "dnf"], "kubectl"),
358
+ "kubectl": (["winget", "chocolatey", "brew", "apt", "dnf"], "kubectl"),
359
+ "helm": (["winget", "chocolatey", "brew", "apt", "dnf"], "helm"),
360
+ "terraform": (["winget", "chocolatey", "brew", "apt", "dnf"], "terraform"),
361
+ "aws cli": (["winget", "chocolatey", "brew"], "aws"),
362
+ "postgresql": (["winget", "chocolatey", "brew", "apt", "dnf"], "psql"),
363
+ "mysql": (["winget", "chocolatey", "brew", "apt", "dnf"], "mysql"),
364
+ "sqlite": (["winget", "chocolatey", "brew", "apt", "dnf"], "sqlite3"),
365
+ "mongodb": (["winget", "chocolatey", "brew"], "mongod"),
366
+ "redis": (["winget", "chocolatey", "brew", "apt", "dnf"], "redis-server"),
367
+ "nginx": (["winget", "chocolatey", "brew", "apt", "dnf"], "nginx"),
368
+ "apache": (["winget", "chocolatey", "brew", "apt", "dnf"], "httpd"),
369
+ "openssl": (["winget", "chocolatey", "brew", "apt", "dnf"], "openssl"),
370
+ "7zip": (["winget", "chocolatey"], "7z"),
371
+ "vlc": (["winget", "chocolatey", "brew"], "vlc"),
372
+ "gimp": (["winget", "chocolatey", "brew", "apt", "dnf"], "gimp"),
373
+ "inkscape": (["winget", "chocolatey", "brew", "apt", "dnf"], "inkscape"),
374
+ "blender": (["winget", "chocolatey", "brew", "apt", "dnf"], "blender"),
375
+ "arduino": (["winget", "chocolatey", "brew"], "arduino-cli"),
376
+ }
377
+
378
+ # C/C++ libraries commonly installed via vcpkg on Windows.
379
+ _VCPKG_LIBS = {
380
+ "boost", "openssl", "zlib", "sqlite3", "curl", "libcurl", "fmt",
381
+ "spdlog", "gtest", "cpr", "nlohmann-json", "eigen3", "opencv",
382
+ }
383
+
384
+ # ------------------------------------------------------------ request model --
385
+ class PackageRequest:
386
+ """One parsed install request. `manager` may be None (meaning
387
+ 'unknown — research first'), `version` defaults to 'latest'."""
388
+
389
+ __slots__ = ("name", "version", "manager", "manager_hint", "target", "raw")
390
+
391
+ def __init__(self, name, version="latest", manager=None, manager_hint=None,
392
+ target=None, raw=""):
393
+ self.name = name
394
+ self.version = version or "latest"
395
+ self.manager = manager
396
+ self.manager_hint = manager_hint
397
+ self.target = target # None | "local" (npm-local etc.)
398
+ self.raw = raw
399
+
400
+ def label(self):
401
+ return f"{self.name}@{self.version}" if self.version != "latest" else self.name
402
+
403
+ def __repr__(self):
404
+ return f"PackageRequest({self.label()!r}, manager={self.manager!r})"
405
+
406
+
407
+ # ---------------------------------------------------------------- parsing --
408
+ _MANAGER_WORDS = {
409
+ "pip": "pip", "pipx": "pipx", "uv": "uv", "conda": "conda",
410
+ "npm": "npm", "pnpm": "pnpm", "yarn": "yarn", "bun": "bun",
411
+ "cargo": "cargo", "go": "go", "composer": "composer",
412
+ "vcpkg": "vcpkg", "chocolatey": "choco", "choco": "chocolatey",
413
+ "winget": "winget", "apt": "apt", "apt-get": "apt",
414
+ "dnf": "dnf", "brew": "brew", "homebrew": "brew",
415
+ }
416
+
417
+ _INSTALL_RE = re.compile(
418
+ r"\b(?:install|setup|get)\b.*?\b(\S+)\b", re.I)
419
+
420
+
421
+ def parse_request(text):
422
+ """Parse free text like 'install latest opencv' / 'Install numpy with
423
+ pip' / 'install NodeJS' into a PackageRequest (manager may be None).
424
+ Returns None if the text doesn't look like an install request at
425
+ all."""
426
+ if not text:
427
+ return None
428
+ low = text.lower().strip()
429
+ m = re.search(r"\b(?:install|setup|get)\b", low)
430
+ if not m:
431
+ return None
432
+
433
+ # Explicit manager mention: "install X with pip", "pip install X",
434
+ # "install X using npm" ...
435
+ manager_hint = None
436
+ for word, manager in _MANAGER_WORDS.items():
437
+ if re.search(rf"\b{re.escape(word)}\b", low):
438
+ manager_hint = manager
439
+ break
440
+
441
+ # The package name: first word after the install/setup/get verb.
442
+ rest = low[m.end():]
443
+ rest = rest.replace("latest", " ").strip()
444
+ name_match = re.search(r"^\s*([a-z0-9][a-z0-9+_.@/-]*)", rest)
445
+ if not name_match:
446
+ return None
447
+ name = name_match.group(1).strip(" ,.:;!?")
448
+ if name in ("a", "an", "the", "package", "packages", "library", "librarys", "tool"):
449
+ nxt = re.search(r"^\s*[a-z0-9]+", rest[name_match.end():])
450
+ if nxt:
451
+ name = nxt.group(0)
452
+ if not name or not re.search(r"[a-z0-9]", name):
453
+ return None
454
+
455
+ # "latest" version request: "install latest opencv" — the spec's own
456
+ # example. The word was stripped above; if it was present, name now
457
+ # holds the real package and version stays "latest".
458
+ version = "latest"
459
+ ver_match = re.search(r"\b([0-9][0-9a-zA-Z_.+-]*)\s*$", rest)
460
+ if ver_match and re.match(r"^[0-9]", name_match.group(1)):
461
+ pass # name started with a digit (e.g. "3d"), don't treat as version
462
+
463
+ target = None
464
+ if re.search(r"\blocal(ly)?\b|\bproject\b", rest):
465
+ target = "local"
466
+
467
+ return PackageRequest(name=name, version=version, manager=None,
468
+ manager_hint=manager_hint, target=target, raw=text)
469
+
470
+
471
+ # ------------------------------------------------------------ detection --
472
+ def manager_available(manager):
473
+ """True if `manager` can be invoked on this machine (binary on PATH,
474
+ or for pip: this interpreter is usable)."""
475
+ if manager not in MANAGERS:
476
+ return False
477
+ spec = MANAGERS[manager]
478
+ if spec["platforms"]:
479
+ if "win" in spec["platforms"] and not _WIN:
480
+ return False
481
+ if "linux" in spec["platforms"] and not _LINUX:
482
+ return False
483
+ if "darwin" in spec["platforms"] and not _MAC:
484
+ return False
485
+ if manager == "pip":
486
+ try:
487
+ import importlib.util
488
+ return importlib.util.find_spec("pip") is not None
489
+ except Exception:
490
+ return False
491
+ return shutil.which(_binary_for(manager)) is not None
492
+
493
+
494
+ def _binary_for(manager):
495
+ return {"chocolatey": "choco", "apt": "apt-get", "dnf": "dnf",
496
+ "brew": "brew", "pipx": "pipx", "uv": "uv", "conda": "conda",
497
+ "npm": "npm", "pnpm": "pnpm", "yarn": "yarn", "bun": "bun",
498
+ "cargo": "cargo", "go": "go", "composer": "composer",
499
+ "vcpkg": "vcpkg", "winget": "winget"}.get(manager, manager)
500
+
501
+
502
+ def _native_system_manager():
503
+ """The best system-package manager for this OS, or None."""
504
+ order = ["winget", "chocolatey", "brew", "apt", "dnf"]
505
+ for m in order:
506
+ if m in MANAGERS and manager_available(m):
507
+ return m
508
+ return None
509
+
510
+
511
+ def detect_manager(req):
512
+ """Pick the manager for a PackageRequest using known-package tables,
513
+ the explicit hint, and the platform's system manager. Returns the
514
+ manager key, or None if the package is unknown/ambiguous."""
515
+ if req.manager:
516
+ return req.manager
517
+ if req.manager_hint:
518
+ return req.manager_hint if req.manager_hint in MANAGERS else None
519
+
520
+ name = req.name.lower()
521
+ # Scoped npm packages ("@scope/name") are unambiguous.
522
+ if name.startswith("@"):
523
+ return "npm"
524
+ # Standalone system tools (NodeJS, Rust, Docker, CUDA...).
525
+ for key, (cands, _binary) in SYSTEM_TOOLS.items():
526
+ if name == key or name.replace("-", " ") == key:
527
+ for c in cands:
528
+ if c == "rustup":
529
+ if shutil.which("rustup"):
530
+ return "rustup"
531
+ continue
532
+ if c in MANAGERS and manager_available(c):
533
+ return c
534
+ sys_mgr = _native_system_manager()
535
+ if sys_mgr:
536
+ return sys_mgr
537
+ return "winget" if _WIN else ("brew" if _MAC else "apt")
538
+ # Known ecosystems.
539
+ if name in PYTHON_PACKAGES:
540
+ return "pip"
541
+ if name in NODE_PACKAGES:
542
+ return "npm"
543
+ if name in RUST_CRATES:
544
+ return "cargo"
545
+ if name in GO_MODULES:
546
+ return "go"
547
+ if name in PHP_PACKAGES:
548
+ return "composer"
549
+ if name in _VCPKG_LIBS and _WIN:
550
+ return "vcpkg"
551
+ # Extension-based hints.
552
+ if name.startswith("lib") or name.endswith("-dev"):
553
+ return "apt" if _LINUX else None
554
+ return None
555
+
556
+
557
+ def resolve_request(text, prefer=None):
558
+ """One-stop parse + detect. Returns (request, manager_key_or_None).
559
+ `prefer` is a manager the caller wants to try first (e.g. from a
560
+ research recommendation). Falls back to treating a bare package
561
+ name as "install <name>" (the /install flow passes bare names)."""
562
+ req = parse_request(text)
563
+ if req is None:
564
+ req = parse_request("install " + text)
565
+ if req is None:
566
+ return None, None
567
+ if prefer and prefer in MANAGERS:
568
+ req.manager = prefer
569
+ return req, prefer
570
+ return req, detect_manager(req)
571
+
572
+
573
+ # ------------------------------------------------------------ execution --
574
+ _INSTALL_TIMEOUT = 900 # 15 minutes wall clock — big SDKs take a while
575
+
576
+ _RUNNABLE_EXTS = (".exe", ".bat", ".cmd", ".sh") if _WIN else ()
577
+
578
+
579
+ def run_install(req, on_event=None, cwd=None, timeout=_INSTALL_TIMEOUT,
580
+ capture=True):
581
+ """Runs the real installer for `req` (req.manager must be set).
582
+ Streams Installation-Dashboard events through `on_event` and returns
583
+ a summary dict:
584
+ {"ok": bool, "manager": str, "command": [argv...],
585
+ "exit_code": int, "timed_out": bool, "output_tail": str,
586
+ "duration": float, "verify": {..} or None,
587
+ "deps_installed": int, "conflicts": bool}
588
+ Never raises for install failures — a failed install is a result,
589
+ not an exception. Also records the outcome in the package history
590
+ (v0.7.7 spec section 8)."""
591
+ result = _run_install_impl(req, on_event=on_event, cwd=cwd,
592
+ timeout=timeout, capture=capture)
593
+ try:
594
+ record_install(result, req)
595
+ except Exception:
596
+ pass
597
+ return result
598
+
599
+
600
+ def _run_install_impl(req, on_event=None, cwd=None, timeout=_INSTALL_TIMEOUT,
601
+ capture=True):
602
+ from . import sound
603
+ manager = req.manager
604
+ if manager not in MANAGERS:
605
+ if on_event:
606
+ on_event({"stage": STAGE_FAILED,
607
+ "message": f"Unknown package manager '{manager}'."})
608
+ return {"ok": False, "manager": manager, "command": [],
609
+ "exit_code": -1, "timed_out": False, "output_tail": "",
610
+ "duration": 0.0, "verify": None,
611
+ "deps_installed": 0, "conflicts": False}
612
+ spec = MANAGERS[manager]
613
+ command = spec["install"](req.name, req.version, req.target)
614
+ if on_event:
615
+ on_event({"stage": STAGE_DOWNLOAD,
616
+ "message": f"Installing {req.label()} via {spec['label']}",
617
+ "detail": " ".join(command)})
618
+ t0 = time.time()
619
+ output_lines = []
620
+ try:
621
+ proc = subprocess.Popen(
622
+ command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
623
+ text=True, encoding="utf-8", errors="replace",
624
+ env={**os.environ, "PYTHONIOENCODING": "utf-8"})
625
+ assert proc.stdout is not None
626
+ for line in proc.stdout:
627
+ line = line.rstrip("\n")
628
+ output_lines.append(line)
629
+ if on_event:
630
+ on_event({"stage": STAGE_RUNNING, "message": line})
631
+ proc.wait(timeout=timeout)
632
+ except subprocess.TimeoutExpired:
633
+ try:
634
+ proc.kill()
635
+ except Exception:
636
+ pass
637
+ duration = round(time.time() - t0, 2)
638
+ tail = "\n".join(output_lines[-20:])
639
+ if on_event:
640
+ on_event({"stage": STAGE_FAILED,
641
+ "message": f"Install timed out after {timeout}s."})
642
+ sound.play("error")
643
+ return {"ok": False, "manager": manager, "command": command,
644
+ "exit_code": 124, "timed_out": True, "output_tail": tail,
645
+ "duration": duration, "verify": None,
646
+ "deps_installed": 0, "conflicts": False}
647
+ except OSError as e:
648
+ duration = round(time.time() - t0, 2)
649
+ if on_event:
650
+ on_event({"stage": STAGE_FAILED,
651
+ "message": f"Could not run {' '.join(command)}: {e}"})
652
+ sound.play("error")
653
+ return {"ok": False, "manager": manager, "command": command,
654
+ "exit_code": -1, "timed_out": False,
655
+ "output_tail": f"Could not run: {e}", "duration": duration,
656
+ "verify": None, "deps_installed": 0, "conflicts": False}
657
+ duration = round(time.time() - t0, 2)
658
+ ok = proc.returncode == 0
659
+ tail = "\n".join(output_lines[-20:])
660
+ deps_count, conflicts = _analyze_output(output_lines, requested=req.name)
661
+ if ok:
662
+ if on_event:
663
+ on_event({"stage": STAGE_VERIFYING,
664
+ "message": f"{manager} reported success — verifying..."})
665
+ verify = verify_package(manager, req.name, req.version)
666
+ if verify and verify.get("ok"):
667
+ if on_event:
668
+ on_event({"stage": STAGE_DONE,
669
+ "message": f"Installed {req.label()} \u2014 verified "
670
+ f"{verify.get('verified', '')}"})
671
+ sound.play("success")
672
+ else:
673
+ note = (verify or {}).get("note") or "verification inconclusive"
674
+ if on_event:
675
+ on_event({"stage": STAGE_DONE,
676
+ "message": f"Install command finished for {req.label()} "
677
+ f"({note}) \u2014 treat as installed."})
678
+ sound.play("success")
679
+ return {"ok": True, "manager": manager, "command": command,
680
+ "exit_code": 0, "timed_out": False, "output_tail": tail,
681
+ "duration": duration, "verify": verify,
682
+ "deps_installed": deps_count, "conflicts": conflicts}
683
+ if on_event:
684
+ on_event({"stage": STAGE_FAILED,
685
+ "message": f"Install failed (exit {proc.returncode})."})
686
+ sound.play("error")
687
+ return {"ok": False, "manager": manager, "command": command,
688
+ "exit_code": proc.returncode, "timed_out": False,
689
+ "output_tail": tail, "duration": duration, "verify": None,
690
+ "deps_installed": deps_count, "conflicts": conflicts}
691
+
692
+
693
+ def _analyze_output(output_lines, requested=None):
694
+ """Best-effort read of the installer's own output:
695
+ - `deps_installed`: count of dependencies pip/npm report
696
+ installing (total minus the requested package itself);
697
+ - `conflicts`: whether the output mentions dependency conflicts.
698
+ Both honest heuristics over real output — 0/False when nothing is
699
+ reported (never fabricated)."""
700
+ text = "\n".join(output_lines)
701
+ deps = 0
702
+ for line in output_lines:
703
+ low = line.strip().lower()
704
+ if low.startswith("successfully installed "):
705
+ parts = line.split("Successfully installed ", 1)[1].split()
706
+ names = [p for p in parts if p and p not in ("and",)]
707
+ if requested:
708
+ wanted = requested.lower().replace("_", "-")
709
+ names = [n for n in names if n.split("==")[0].lower() != wanted]
710
+ deps = max(deps, len(names))
711
+ elif "added " in low and " packages" in low:
712
+ import re
713
+ m = re.search(r"added (\d+) packages", low)
714
+ if m:
715
+ deps = max(deps, max(int(m.group(1)) - 1, 0))
716
+ conflicts = "conflict" in text.lower()
717
+ return deps, conflicts
718
+
719
+
720
+ def verify_package(manager, name, version="latest"):
721
+ """Post-install verification (spec section 12): the manager's own
722
+ list/show query, plus (for pip-family managers) a real import test.
723
+ Returns {"ok": bool, "verified": str, "note": str} — never raises."""
724
+ if manager not in MANAGERS:
725
+ return {"ok": False, "verified": "", "note": f"unknown manager {manager}"}
726
+ spec = MANAGERS[manager]
727
+ try:
728
+ cmd = spec["verify"](name, version)
729
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60,
730
+ encoding="utf-8", errors="replace")
731
+ listed = result.returncode == 0 and bool((result.stdout or "").strip())
732
+ except Exception as e:
733
+ return {"ok": False, "verified": "", "note": f"verify command failed: {e}"}
734
+
735
+ # Import check for pip-family managers (spec section 12: "Import
736
+ # succeeds").
737
+ import_name = None
738
+ if spec.get("check_import"):
739
+ import_name = _IMPORT_ALIASES.get(name, name.replace("-", "_").lower())
740
+ try:
741
+ import importlib
742
+ importlib.import_module(import_name)
743
+ imported = True
744
+ except Exception:
745
+ imported = False
746
+ else:
747
+ imported = True
748
+
749
+ ok = listed and imported
750
+ pieces = []
751
+ if listed:
752
+ pieces.append(f"listed by {manager}")
753
+ if spec.get("check_import"):
754
+ pieces.append(f"import {import_name} "
755
+ + ("succeeded" if imported else "failed"))
756
+ if not listed:
757
+ pieces.append(f"{manager} did not list it (may still be usable)")
758
+ return {"ok": ok, "verified": ", ".join(pieces), "note": " ".join(pieces)}
759
+
760
+
761
+ def importable_name(package_name):
762
+ return _IMPORT_ALIASES.get(package_name,
763
+ package_name.replace("-", "_").lower())
764
+
765
+
766
+ # ------------------------------------------------------------ permissions --
767
+ _DECISIONS_STORE = os.path.join(os.path.expanduser("~"), ".cct_package_decisions.json")
768
+
769
+
770
+ def _load_decisions():
771
+ try:
772
+ with open(_DECISIONS_STORE, "r", encoding="utf-8") as f:
773
+ return json.load(f)
774
+ except Exception:
775
+ return {}
776
+
777
+
778
+ def _save_decisions(data):
779
+ try:
780
+ with open(_DECISIONS_STORE, "w", encoding="utf-8") as f:
781
+ json.dump(data, f, indent=2)
782
+ return True
783
+ except Exception:
784
+ return False
785
+
786
+
787
+ def remembered_decision(manager, name):
788
+ """'allow' / 'deny' / None — the persisted always-remembered choice
789
+ for this exact manager+package pair, if the user picked one in a
790
+ previous install flow (spec section 3: "Remember choice if
791
+ selected")."""
792
+ return _load_decisions().get(f"{manager}|{name.lower()}")
793
+
794
+
795
+ def remember_decision(manager, name, decision):
796
+ key = f"{manager}|{name.lower()}"
797
+ data = _load_decisions()
798
+ data[key] = decision
799
+ _save_decisions(data)
800
+
801
+
802
+ def forget_decision(manager, name):
803
+ data = _load_decisions()
804
+ data.pop(f"{manager}|{name.lower()}", None)
805
+ _save_decisions(data)
806
+
807
+
808
+ def list_remembered():
809
+ """[(manager, name, decision), ...] for the /packages settings view."""
810
+ return [(k.split("|", 1)[0], k.split("|", 1)[1], v)
811
+ for k, v in _load_decisions().items() if "|" in k]
812
+
813
+
814
+ # -------------------------------------------------------------- dashboard --
815
+ def dashboard_lines(events):
816
+ """Renderer for the classic terminal: turns a list of events into
817
+ the Installation Dashboard lines (spec section 11) — package, manager,
818
+ command, live output, verification, completion status."""
819
+ from . import theme
820
+ lines = []
821
+ for ev in events:
822
+ stage = ev.get("stage")
823
+ msg = ev.get("message", "")
824
+ if stage == STAGE_RUNNING:
825
+ lines.append(theme.dim(" " + msg))
826
+ elif stage == STAGE_DOWNLOAD:
827
+ lines.append(theme.cyan(" \u2b07 ") + msg +
828
+ (theme.faint(" " + ev["detail"]) if ev.get("detail") else ""))
829
+ elif stage == STAGE_VERIFYING:
830
+ lines.append(theme.purple(" \U0001f50d ") + msg)
831
+ elif stage == STAGE_DONE:
832
+ lines.append(theme.green(" \u2713 ") + msg)
833
+ elif stage == STAGE_FAILED:
834
+ lines.append(theme.red(" \u2717 ") + msg)
835
+ elif stage == STAGE_PERMISSION:
836
+ lines.append(theme.orange(" \U0001f511 ") + msg)
837
+ elif stage == STAGE_SKIPPED:
838
+ lines.append(theme.faint(" \u2014 ") + msg)
839
+ else:
840
+ lines.append(theme.text(" " + msg))
841
+ return lines
842
+
843
+
844
+ # ---------------------------------------------------------------- summary --
845
+ def _env_label():
846
+ """Python version + platform for install summaries (spec section 7's
847
+ 'Environment' row)."""
848
+ import platform
849
+ try:
850
+ return f"Python {platform.python_version()} \u00b7 {platform.system()}"
851
+ except Exception:
852
+ return "Python (unknown)"
853
+
854
+
855
+ def summarize(result, req):
856
+ """Professional install summary card (spec section 7) — the
857
+ classic REPL prints this in a panel after every install, and it
858
+ carries the spec's exact fields. Pure text — callers decide where
859
+ to render it."""
860
+ version = getattr(req, "version", None) or "latest"
861
+ if result.get("ok"):
862
+ verify = result.get("verify") or {}
863
+ lines = ["\u2705 Package Installed Successfully", ""]
864
+ if version != "latest":
865
+ lines.append(f"Package: {req.name} ({version})")
866
+ lines.append(f"Version: {version}")
867
+ else:
868
+ lines.append(f"Package: {req.name}")
869
+ lines.append(f"Environment: {_env_label()}")
870
+ lines.append(f"Dependencies Installed: {result.get('deps_installed', 0)}")
871
+ lines.append(f"Installation Time: {result.get('duration', 0)}s")
872
+ if verify.get("verified"):
873
+ lines.append(f"Verification: \U0001f50d {verify['verified']}")
874
+ elif verify.get("note"):
875
+ lines.append(f"Verification: {verify['note']}")
876
+ else:
877
+ lines.append("Verification: command reported success")
878
+ lines.append("")
879
+ if result.get("conflicts"):
880
+ lines.append("\u26a0 Dependency conflicts were mentioned in the install "
881
+ "output \u2014 review the dashboard log before use.")
882
+ else:
883
+ lines.append("No dependency conflicts detected.")
884
+ lines.append("Ready for use \u2014 ask CAT what to build next.")
885
+ return "\n".join(lines)
886
+ lines = ["\u274c Package Installation Failed", ""]
887
+ lines.append(f"Package: {req.name}")
888
+ lines.append(f"Manager: {MANAGERS[req.manager]['label']}")
889
+ lines.append(f"Exit code: {result.get('exit_code')}"
890
+ + (" (timed out)" if result.get("timed_out") else ""))
891
+ tail = (result.get("output_tail") or "").strip()
892
+ if tail:
893
+ lines.append("")
894
+ lines.append("Logs (last lines):")
895
+ for line in tail.splitlines()[-6:]:
896
+ lines.append(" " + line)
897
+ lines.append("")
898
+ lines.append("Suggestion: check the package name/version and network, "
899
+ "then retry \u2014 or ask CAT to research alternatives.")
900
+ return "\n".join(lines)
901
+
902
+
903
+ # ---------------------------------------------------------------- history --
904
+ # v0.7.7 spec section 8: persistent package installation history, kept
905
+ # alongside the remembered-decisions store in the same dotfile family.
906
+ HISTORY_PATH = os.path.join(os.path.expanduser("~"), ".cct_package_history.json")
907
+ MAX_HISTORY = 100
908
+
909
+
910
+ def _load_history():
911
+ if not os.path.exists(HISTORY_PATH):
912
+ return []
913
+ try:
914
+ with open(HISTORY_PATH, "r", encoding="utf-8") as f:
915
+ import json as _json
916
+ data = _json.load(f)
917
+ return data if isinstance(data, list) else []
918
+ except Exception:
919
+ return []
920
+
921
+
922
+ def _save_history(records):
923
+ try:
924
+ with open(HISTORY_PATH, "w", encoding="utf-8") as f:
925
+ import json as _json
926
+ _json.dump(records, f, indent=2)
927
+ return True
928
+ except Exception:
929
+ return False
930
+
931
+
932
+ def record_install(result, req):
933
+ """Records one install outcome (called automatically by
934
+ run_install). A reinstall of an already-recorded package updates
935
+ that record's `updated_at` (and version) instead of duplicating it
936
+ (spec section 8's Update semantics)."""
937
+ records = _load_history()
938
+ now = time.time()
939
+ name = getattr(req, "name", "") or ""
940
+ if not name:
941
+ return
942
+ existing = next((r for r in records if r.get("name") == name), None)
943
+ entry = {
944
+ "name": name,
945
+ "version": getattr(req, "version", "latest") or "latest",
946
+ "manager": getattr(req, "manager", "") or "",
947
+ "ok": bool(result.get("ok")),
948
+ "env": _env_label(),
949
+ "installed_at": now,
950
+ "updated_at": now,
951
+ "duration": result.get("duration", 0),
952
+ "verified": bool((result.get("verify") or {}).get("ok")),
953
+ "conflicts": bool(result.get("conflicts")),
954
+ "exit_code": result.get("exit_code"),
955
+ "error_tail": (result.get("output_tail") or "")[-400:] if not result.get("ok") else "",
956
+ "notes": "",
957
+ }
958
+ if existing is not None:
959
+ existing.update({k: v for k, v in entry.items() if k != "installed_at"})
960
+ existing["updated_at"] = now
961
+ existing["version"] = entry["version"]
962
+ else:
963
+ records.insert(0, entry)
964
+ records = records[:MAX_HISTORY]
965
+ _save_history(records)
966
+
967
+
968
+ def package_history(limit=50):
969
+ """All install-history records, newest first."""
970
+ return _load_history()[:limit]
971
+
972
+
973
+ def history_search(query):
974
+ """Filter history by package name or manager (case-insensitive)."""
975
+ q = (query or "").strip().lower()
976
+ if not q:
977
+ return package_history()
978
+ return [r for r in _load_history()
979
+ if q in r.get("name", "").lower() or q in r.get("manager", "").lower()]
980
+
981
+
982
+ def remove_history(name):
983
+ """Removes every history record for `name`. Returns the number of
984
+ records removed. Only touches the history JSON — never the
985
+ installed package itself."""
986
+ records = _load_history()
987
+ kept = [r for r in records if r.get("name") != name]
988
+ removed = len(records) - len(kept)
989
+ if removed:
990
+ _save_history(kept)
991
+ return removed
992
+
993
+
994
+ def history_entry(name):
995
+ """The newest history record for `name`, or None."""
996
+ for r in _load_history():
997
+ if r.get("name") == name:
998
+ return r
999
+ return None
1000
+
1001
+
1002
+ def clear_history():
1003
+ """Empties the whole install history. Only the JSON store — never
1004
+ uninstalls anything. Returns True when there was something to
1005
+ clear."""
1006
+ records = _load_history()
1007
+ if records:
1008
+ _save_history([])
1009
+ return True
1010
+ return False
1011
+
1012
+
1013
+ def announce(result, req):
1014
+ """Short single-line status for the activity log / event bus."""
1015
+ from . import sound
1016
+ if result.get("ok"):
1017
+ sound.play("success")
1018
+ eventbus.bus.publish(eventbus.PACKAGE_INSTALLED,
1019
+ package=req.label(), manager=req.manager or "")
1020
+ return f"Installed {req.label()} via {req.manager}"
1021
+ sound.play("error")
1022
+ eventbus.bus.publish(eventbus.PACKAGE_FAILED,
1023
+ package=req.label(), manager=req.manager or "")
1024
+ return f"Failed to install {req.label()} via {req.manager}"