painapple-code 1.0.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 (436) hide show
  1. painapple_code/__init__.py +30 -0
  2. painapple_code/__main__.py +18 -0
  3. painapple_code/_version.py +24 -0
  4. painapple_code/auth_middleware.py +930 -0
  5. painapple_code/cli/__init__.py +204 -0
  6. painapple_code/cli/deploy/__init__.py +15 -0
  7. painapple_code/cli/deploy/claude_seed.py +41 -0
  8. painapple_code/cli/deploy/config.py +260 -0
  9. painapple_code/cli/deploy/container.py +715 -0
  10. painapple_code/cli/deploy/launch.py +163 -0
  11. painapple_code/cli/deploy/runtime.py +223 -0
  12. painapple_code/cli/lifecycle_cmd.py +443 -0
  13. painapple_code/cli/list_cmd.py +572 -0
  14. painapple_code/cli/manage_cmd.py +670 -0
  15. painapple_code/cli/netinfo.py +125 -0
  16. painapple_code/cli/profiles.py +390 -0
  17. painapple_code/cli/serve_args.py +163 -0
  18. painapple_code/cli/serve_config.py +256 -0
  19. painapple_code/cli/setup_cmd.py +664 -0
  20. painapple_code/cli/ui.py +438 -0
  21. painapple_code/cost_analytics.py +879 -0
  22. painapple_code/data/models.defaults.yaml +18 -0
  23. painapple_code/data/models.yaml +18 -0
  24. painapple_code/data/presets.defaults.json +32 -0
  25. painapple_code/data/strings.yaml +2269 -0
  26. painapple_code/helpers.py +406 -0
  27. painapple_code/paths.py +1514 -0
  28. painapple_code/prompt_explorer.py +984 -0
  29. painapple_code/providers/__init__.py +224 -0
  30. painapple_code/providers/base.py +710 -0
  31. painapple_code/providers/claude/__init__.py +74 -0
  32. painapple_code/providers/claude/capabilities.py +218 -0
  33. painapple_code/providers/claude/errors.py +79 -0
  34. painapple_code/providers/claude/launch.py +50 -0
  35. painapple_code/providers/claude/summary.py +89 -0
  36. painapple_code/providers/claude/translate.py +74 -0
  37. painapple_code/providers/claude_sdk/__init__.py +139 -0
  38. painapple_code/providers/claude_sdk/driver.py +412 -0
  39. painapple_code/providers/codex/__init__.py +87 -0
  40. painapple_code/providers/codex/capabilities.py +321 -0
  41. painapple_code/providers/codex/errors.py +70 -0
  42. painapple_code/providers/codex/launch.py +142 -0
  43. painapple_code/providers/codex/summary.py +196 -0
  44. painapple_code/providers/codex/translate.py +204 -0
  45. painapple_code/providers/codex_app_server/__init__.py +100 -0
  46. painapple_code/providers/codex_app_server/capabilities.py +41 -0
  47. painapple_code/providers/codex_app_server/launch.py +202 -0
  48. painapple_code/providers/codex_app_server/summary.py +291 -0
  49. painapple_code/providers/codex_app_server/translate.py +263 -0
  50. painapple_code/providers/codex_app_server/transport.py +238 -0
  51. painapple_code/routes/__init__.py +0 -0
  52. painapple_code/routes/api_agents.py +441 -0
  53. painapple_code/routes/api_app.py +368 -0
  54. painapple_code/routes/api_app_commit_sections.py +127 -0
  55. painapple_code/routes/api_app_config.py +997 -0
  56. painapple_code/routes/api_app_session_prefs.py +217 -0
  57. painapple_code/routes/api_browser.py +690 -0
  58. painapple_code/routes/api_commands.py +374 -0
  59. painapple_code/routes/api_costs.py +128 -0
  60. painapple_code/routes/api_drafts.py +139 -0
  61. painapple_code/routes/api_exec.py +149 -0
  62. painapple_code/routes/api_files.py +542 -0
  63. painapple_code/routes/api_git.py +657 -0
  64. painapple_code/routes/api_logs.py +649 -0
  65. painapple_code/routes/api_plugins.py +275 -0
  66. painapple_code/routes/api_project_config.py +418 -0
  67. painapple_code/routes/api_prompts.py +231 -0
  68. painapple_code/routes/api_providers.py +53 -0
  69. painapple_code/routes/api_search.py +411 -0
  70. painapple_code/routes/api_session_stash.py +234 -0
  71. painapple_code/routes/api_session_welcome.py +151 -0
  72. painapple_code/routes/api_sessions.py +742 -0
  73. painapple_code/routes/api_shadow.py +323 -0
  74. painapple_code/routes/api_shadow_db.py +512 -0
  75. painapple_code/routes/api_shadow_files.py +332 -0
  76. painapple_code/routes/api_shadow_search.py +476 -0
  77. painapple_code/routes/api_skills.py +610 -0
  78. painapple_code/routes/api_tasks.py +147 -0
  79. painapple_code/routes/api_terminal.py +540 -0
  80. painapple_code/routes/api_upload.py +293 -0
  81. painapple_code/routes/api_viewer.py +229 -0
  82. painapple_code/routes/dependencies.py +109 -0
  83. painapple_code/routes/ws_chat.py +837 -0
  84. painapple_code/server.py +2208 -0
  85. painapple_code/server_logging.py +263 -0
  86. painapple_code/services/__init__.py +0 -0
  87. painapple_code/services/agent_session.py +2738 -0
  88. painapple_code/session_store.py +253 -0
  89. painapple_code/session_store_core.py +879 -0
  90. painapple_code/shadow_db.py +830 -0
  91. painapple_code/shadow_db_plans.py +369 -0
  92. painapple_code/shadow_db_queries.py +595 -0
  93. painapple_code/shadow_db_schema.py +676 -0
  94. painapple_code/shadow_git.py +1296 -0
  95. painapple_code/shadow_git_frontmatter.py +314 -0
  96. painapple_code/shadow_git_sections.py +520 -0
  97. painapple_code/shadow_git_summary.py +462 -0
  98. painapple_code/shadow_parser.py +560 -0
  99. painapple_code/static/css/00-variables.css +276 -0
  100. painapple_code/static/css/01-base.css +245 -0
  101. painapple_code/static/css/02-tooltips.css +36 -0
  102. painapple_code/static/css/09-left-rail.css +448 -0
  103. painapple_code/static/css/10-header.css +777 -0
  104. painapple_code/static/css/11-connection-bar.css +120 -0
  105. painapple_code/static/css/20-messages.css +750 -0
  106. painapple_code/static/css/21-tool-blocks.css +2340 -0
  107. painapple_code/static/css/22-edit-diff.css +619 -0
  108. painapple_code/static/css/23-thinking.css +1584 -0
  109. painapple_code/static/css/24-background-tasks.css +359 -0
  110. painapple_code/static/css/25-context-block.css +252 -0
  111. painapple_code/static/css/26-session-setup.css +144 -0
  112. painapple_code/static/css/30-markdown.css +854 -0
  113. painapple_code/static/css/40-input.css +2418 -0
  114. painapple_code/static/css/41-autocomplete.css +127 -0
  115. painapple_code/static/css/42-activity-strip.css +97 -0
  116. painapple_code/static/css/43-file-autocomplete.css +121 -0
  117. painapple_code/static/css/44-snippets-autocomplete.css +224 -0
  118. painapple_code/static/css/45-selection-bar.css +433 -0
  119. painapple_code/static/css/46-keyboard-bar.css +119 -0
  120. painapple_code/static/css/47-keyboard-longpress.css +116 -0
  121. painapple_code/static/css/47-token-profile.css +106 -0
  122. painapple_code/static/css/50-welcome.css +3095 -0
  123. painapple_code/static/css/51-modals.css +301 -0
  124. painapple_code/static/css/52-utilities.css +77 -0
  125. painapple_code/static/css/53-image-upload.css +456 -0
  126. painapple_code/static/css/54-todo-list.css +102 -0
  127. painapple_code/static/css/55-file-upload.css +113 -0
  128. painapple_code/static/css/56-chat-navigator.css +218 -0
  129. painapple_code/static/css/56-permission-card.css +332 -0
  130. painapple_code/static/css/56-plan-approval.css +204 -0
  131. painapple_code/static/css/56-question-form.css +602 -0
  132. painapple_code/static/css/57-swipe-indicator.css +95 -0
  133. painapple_code/static/css/58-session-families.css +627 -0
  134. painapple_code/static/css/59-about.css +196 -0
  135. painapple_code/static/css/61-log-explorer.css +723 -0
  136. painapple_code/static/css/62-json-tree.css +489 -0
  137. painapple_code/static/css/62-skills-widget.css +650 -0
  138. painapple_code/static/css/63-commands-widget.css +30 -0
  139. painapple_code/static/css/63-plugins-widget.css +174 -0
  140. painapple_code/static/css/63-terminal.css +516 -0
  141. painapple_code/static/css/64-search-files-widget.css +448 -0
  142. painapple_code/static/css/65-snippets-widget.css +426 -0
  143. painapple_code/static/css/66-active-sessions.css +979 -0
  144. painapple_code/static/css/66-agents-widget.css +265 -0
  145. painapple_code/static/css/66-file-explorer-widget.css +855 -0
  146. painapple_code/static/css/66-git-panel.css +791 -0
  147. painapple_code/static/css/66-history-explorer.css +1496 -0
  148. painapple_code/static/css/67-diff-viewer.css +602 -0
  149. painapple_code/static/css/67-editor-view.css +748 -0
  150. painapple_code/static/css/67-file-preview-widget.css +1873 -0
  151. painapple_code/static/css/68-browser-widget.css +337 -0
  152. painapple_code/static/css/68-compare-wizard.css +355 -0
  153. painapple_code/static/css/68-csv-preview.css +106 -0
  154. painapple_code/static/css/68-diff-preview.css +103 -0
  155. painapple_code/static/css/68-helpers-install-widget.css +491 -0
  156. painapple_code/static/css/68-image-annotate.css +346 -0
  157. painapple_code/static/css/68-terminal-view.css +98 -0
  158. painapple_code/static/css/68-uploads-widget.css +229 -0
  159. painapple_code/static/css/69-cost-analytics.css +537 -0
  160. painapple_code/static/css/70-config-panel.css +3736 -0
  161. painapple_code/static/css/70-excalidraw.css +136 -0
  162. painapple_code/static/css/70-prompt-explorer.css +890 -0
  163. painapple_code/static/css/71-chart.css +111 -0
  164. painapple_code/static/css/72-discussion-sidebar.css +678 -0
  165. painapple_code/static/css/74-debug-logs.css +656 -0
  166. painapple_code/static/css/80-open-dialog.css +146 -0
  167. painapple_code/static/css/80-quick-switcher.css +383 -0
  168. painapple_code/static/css/80-widget-system.css +1763 -0
  169. painapple_code/static/css/81-quick-actions.css +1020 -0
  170. painapple_code/static/css/82-context-popover.css +1233 -0
  171. painapple_code/static/css/83-context-menu.css +339 -0
  172. painapple_code/static/css/85-lazy-loading.css +102 -0
  173. painapple_code/static/css/86-zen-mode.css +708 -0
  174. painapple_code/static/css/87-grid-switcher.css +346 -0
  175. painapple_code/static/css/login.css +198 -0
  176. painapple_code/static/dist/app.bundle.js +7177 -0
  177. painapple_code/static/feature-triage.html +446 -0
  178. painapple_code/static/icons/apple-touch-icon.png +0 -0
  179. painapple_code/static/icons/favicon-32.png +0 -0
  180. painapple_code/static/icons/favicon.ico +0 -0
  181. painapple_code/static/icons/icon-128.png +0 -0
  182. painapple_code/static/icons/icon-144.png +0 -0
  183. painapple_code/static/icons/icon-152.png +0 -0
  184. painapple_code/static/icons/icon-180.png +0 -0
  185. painapple_code/static/icons/icon-192.png +0 -0
  186. painapple_code/static/icons/icon-384.png +0 -0
  187. painapple_code/static/icons/icon-512.png +0 -0
  188. painapple_code/static/icons/icon-72.png +0 -0
  189. painapple_code/static/icons/icon-96.png +0 -0
  190. painapple_code/static/js/action-delegate.js +187 -0
  191. painapple_code/static/js/activity-strip.js +193 -0
  192. painapple_code/static/js/app/context-menus.js +619 -0
  193. painapple_code/static/js/app/file-preview.js +163 -0
  194. painapple_code/static/js/app/init-ui.js +574 -0
  195. painapple_code/static/js/app/input-events.js +561 -0
  196. painapple_code/static/js/app/input-resize.js +313 -0
  197. painapple_code/static/js/app/message-actions.js +419 -0
  198. painapple_code/static/js/app/panels.js +210 -0
  199. painapple_code/static/js/app/rendering-delegators.js +246 -0
  200. painapple_code/static/js/app/server-loading.js +365 -0
  201. painapple_code/static/js/app/session-ops.js +287 -0
  202. painapple_code/static/js/app/session-switch.js +443 -0
  203. painapple_code/static/js/app/settings-helpers.js +280 -0
  204. painapple_code/static/js/app/startup-welcome.js +290 -0
  205. painapple_code/static/js/app-context.js +157 -0
  206. painapple_code/static/js/app.js +2134 -0
  207. painapple_code/static/js/auth-fetch.js +46 -0
  208. painapple_code/static/js/background-tasks.js +274 -0
  209. painapple_code/static/js/boot.js +73 -0
  210. painapple_code/static/js/caret-position.js +171 -0
  211. painapple_code/static/js/chat-navigator.js +710 -0
  212. painapple_code/static/js/chat-search.js +323 -0
  213. painapple_code/static/js/command-executor.js +176 -0
  214. painapple_code/static/js/command-store.js +497 -0
  215. painapple_code/static/js/components.js +1414 -0
  216. painapple_code/static/js/config.js +218 -0
  217. painapple_code/static/js/context-menu.js +740 -0
  218. painapple_code/static/js/controllers/chat-controller.js +4252 -0
  219. painapple_code/static/js/controllers/dialog-controller.js +217 -0
  220. painapple_code/static/js/controllers/tab-controller.js +1828 -0
  221. painapple_code/static/js/controllers/thinking-controller.js +915 -0
  222. painapple_code/static/js/diff-utils.js +486 -0
  223. painapple_code/static/js/drafts-pill.js +95 -0
  224. painapple_code/static/js/editor-view.js +815 -0
  225. painapple_code/static/js/effort-settings.js +430 -0
  226. painapple_code/static/js/feature-triage.js +811 -0
  227. painapple_code/static/js/file-autocomplete.js +516 -0
  228. painapple_code/static/js/file-tabs.js +123 -0
  229. painapple_code/static/js/gestures.js +448 -0
  230. painapple_code/static/js/grid-switcher.js +334 -0
  231. painapple_code/static/js/input-handler.js +1472 -0
  232. painapple_code/static/js/keyboard-bar.js +495 -0
  233. painapple_code/static/js/layout-switcher.js +109 -0
  234. painapple_code/static/js/linkify-utils.js +200 -0
  235. painapple_code/static/js/login.js +141 -0
  236. painapple_code/static/js/open-dialog.js +879 -0
  237. painapple_code/static/js/orphan-terminals.js +373 -0
  238. painapple_code/static/js/path-utils.js +179 -0
  239. painapple_code/static/js/perf-marks.js +102 -0
  240. painapple_code/static/js/permission-settings.js +475 -0
  241. painapple_code/static/js/pointer-input.js +59 -0
  242. painapple_code/static/js/preview/json-tree-menu.js +226 -0
  243. painapple_code/static/js/preview/json-tree-toolbar.js +291 -0
  244. painapple_code/static/js/preview/json-tree.js +457 -0
  245. painapple_code/static/js/preview/preview-breadcrumb.js +387 -0
  246. painapple_code/static/js/preview/preview-edit.js +367 -0
  247. painapple_code/static/js/preview/preview-events.js +361 -0
  248. painapple_code/static/js/preview/preview-history.js +567 -0
  249. painapple_code/static/js/preview/preview-inline-edit.js +869 -0
  250. painapple_code/static/js/preview/preview-poll.js +108 -0
  251. painapple_code/static/js/preview/preview-render.js +572 -0
  252. painapple_code/static/js/preview/preview-search.js +421 -0
  253. painapple_code/static/js/preview/preview-state.js +256 -0
  254. painapple_code/static/js/preview/preview-utils.js +251 -0
  255. painapple_code/static/js/preview-plugins/chart-plugin.js +22 -0
  256. painapple_code/static/js/preview-plugins/csv-plugin.js +188 -0
  257. painapple_code/static/js/preview-plugins/diff-plugin.js +258 -0
  258. painapple_code/static/js/preview-plugins/excalidraw-plugin.js +25 -0
  259. painapple_code/static/js/preview-plugins/html-plugin.js +104 -0
  260. painapple_code/static/js/preview-plugins/image-plugin.js +20 -0
  261. painapple_code/static/js/preview-plugins/index.js +34 -0
  262. painapple_code/static/js/preview-plugins/json-plugin.js +99 -0
  263. painapple_code/static/js/preview-plugins/jsonl-plugin.js +152 -0
  264. painapple_code/static/js/preview-plugins/markdown-plugin.js +266 -0
  265. painapple_code/static/js/preview-plugins/panzoom-plugin.js +63 -0
  266. painapple_code/static/js/preview-plugins/plugin-helpers.js +282 -0
  267. painapple_code/static/js/project-color-picker.js +110 -0
  268. painapple_code/static/js/project-colors.js +163 -0
  269. painapple_code/static/js/prompt-favorites.js +187 -0
  270. painapple_code/static/js/quick-actions-menu.js +1736 -0
  271. painapple_code/static/js/quick-actions-registry.js +1239 -0
  272. painapple_code/static/js/quick-switcher/controller.js +294 -0
  273. painapple_code/static/js/quick-switcher/fuzzy-scorer.js +123 -0
  274. painapple_code/static/js/quick-switcher/index.js +30 -0
  275. painapple_code/static/js/quick-switcher/providers/base-provider.js +51 -0
  276. painapple_code/static/js/quick-switcher/providers/command-provider.js +56 -0
  277. painapple_code/static/js/quick-switcher/providers/file-provider.js +291 -0
  278. painapple_code/static/js/quick-switcher/providers/panel-provider.js +132 -0
  279. painapple_code/static/js/quick-switcher/providers/project-provider.js +361 -0
  280. painapple_code/static/js/quick-switcher/providers/read-files-provider.js +104 -0
  281. painapple_code/static/js/quick-switcher/providers/skills-provider.js +129 -0
  282. painapple_code/static/js/quick-switcher/registry.js +69 -0
  283. painapple_code/static/js/quick-switcher/ui/item.js +54 -0
  284. painapple_code/static/js/quick-switcher/ui/picker.js +464 -0
  285. painapple_code/static/js/recent-opens.js +40 -0
  286. painapple_code/static/js/scroll-manager.js +530 -0
  287. painapple_code/static/js/scroll-state-machine.js +359 -0
  288. painapple_code/static/js/selection/action-bar.js +793 -0
  289. painapple_code/static/js/selection/index.js +15 -0
  290. painapple_code/static/js/selection/selection-handler.js +877 -0
  291. painapple_code/static/js/selection/state.js +181 -0
  292. painapple_code/static/js/session/agent-progress.js +202 -0
  293. painapple_code/static/js/session/handle-agent-message.js +481 -0
  294. painapple_code/static/js/session/handle-message.js +550 -0
  295. painapple_code/static/js/session/interactive.js +587 -0
  296. painapple_code/static/js/session/message-store.js +271 -0
  297. painapple_code/static/js/session/messages.js +253 -0
  298. painapple_code/static/js/session/persistence.js +179 -0
  299. painapple_code/static/js/session/restore.js +136 -0
  300. painapple_code/static/js/session/sync.js +333 -0
  301. painapple_code/static/js/session-container-pool.js +371 -0
  302. painapple_code/static/js/session-setup-panel.js +274 -0
  303. painapple_code/static/js/session.js +1047 -0
  304. painapple_code/static/js/shortcut-hints.js +272 -0
  305. painapple_code/static/js/shortcuts.js +1146 -0
  306. painapple_code/static/js/skills-autocomplete.js +262 -0
  307. painapple_code/static/js/snippets-autocomplete.js +738 -0
  308. painapple_code/static/js/stash-refs-view.js +83 -0
  309. painapple_code/static/js/stash-ui.js +768 -0
  310. painapple_code/static/js/stash.js +785 -0
  311. painapple_code/static/js/status-bar.js +1184 -0
  312. painapple_code/static/js/token-profile.js +274 -0
  313. painapple_code/static/js/tool-renderer-blocks.js +1626 -0
  314. painapple_code/static/js/tool-renderer-thinking.js +813 -0
  315. painapple_code/static/js/tool-renderer.js +1500 -0
  316. painapple_code/static/js/tooltips.js +197 -0
  317. painapple_code/static/js/upload-manager.js +1019 -0
  318. painapple_code/static/js/utils.js +521 -0
  319. painapple_code/static/js/welcome/api.js +363 -0
  320. painapple_code/static/js/welcome/cards.js +626 -0
  321. painapple_code/static/js/welcome/context-menu.js +585 -0
  322. painapple_code/static/js/welcome/families.js +801 -0
  323. painapple_code/static/js/welcome/preview.js +234 -0
  324. painapple_code/static/js/welcome/state.js +129 -0
  325. painapple_code/static/js/welcome.js +1874 -0
  326. painapple_code/static/js/widget-system/base-widget.js +717 -0
  327. painapple_code/static/js/widget-system/device-manager.js +178 -0
  328. painapple_code/static/js/widget-system/event-bus.js +152 -0
  329. painapple_code/static/js/widget-system/icons.js +120 -0
  330. painapple_code/static/js/widget-system/index.js +49 -0
  331. painapple_code/static/js/widget-system/init.js +54 -0
  332. painapple_code/static/js/widget-system/types/bottom-sheet.js +307 -0
  333. painapple_code/static/js/widget-system/types/floating.js +915 -0
  334. painapple_code/static/js/widget-system/types/index.js +40 -0
  335. painapple_code/static/js/widget-system/types/modal.js +147 -0
  336. painapple_code/static/js/widget-system/types/sidebar.js +158 -0
  337. painapple_code/static/js/widget-system/types/tab.js +146 -0
  338. painapple_code/static/js/widget-system/types/top-sheet.js +203 -0
  339. painapple_code/static/js/widget-system/widget-manager.js +1187 -0
  340. painapple_code/static/js/widgets/about-widget.js +260 -0
  341. painapple_code/static/js/widgets/active-sessions-widget.js +760 -0
  342. painapple_code/static/js/widgets/agents-widget.js +897 -0
  343. painapple_code/static/js/widgets/browser-widget.js +579 -0
  344. painapple_code/static/js/widgets/commands-widget.js +694 -0
  345. painapple_code/static/js/widgets/config/commit-sections.js +449 -0
  346. painapple_code/static/js/widgets/config/dir-autocomplete.js +224 -0
  347. painapple_code/static/js/widgets/config/gestures.js +161 -0
  348. painapple_code/static/js/widgets/config/models-tab.js +988 -0
  349. painapple_code/static/js/widgets/config/quick-actions-tab.js +882 -0
  350. painapple_code/static/js/widgets/config/shortcut-editor.js +587 -0
  351. painapple_code/static/js/widgets/config/state.js +517 -0
  352. painapple_code/static/js/widgets/config/system-controls.js +90 -0
  353. painapple_code/static/js/widgets/config-widget.js +1535 -0
  354. painapple_code/static/js/widgets/cost-analytics-widget.js +621 -0
  355. painapple_code/static/js/widgets/debug-widget.js +693 -0
  356. painapple_code/static/js/widgets/diff-viewer-widget.js +1420 -0
  357. painapple_code/static/js/widgets/discussion-widget.js +1107 -0
  358. painapple_code/static/js/widgets/file-explorer-widget.js +2166 -0
  359. painapple_code/static/js/widgets/file-preview-widget.js +861 -0
  360. painapple_code/static/js/widgets/git-widget.js +794 -0
  361. painapple_code/static/js/widgets/helpers-install-widget.js +720 -0
  362. painapple_code/static/js/widgets/history-explorer-widget.js +1804 -0
  363. painapple_code/static/js/widgets/image-annotate-widget.js +951 -0
  364. painapple_code/static/js/widgets/image-preview-widget.js +692 -0
  365. painapple_code/static/js/widgets/index.js +176 -0
  366. painapple_code/static/js/widgets/log-explorer-widget.js +1057 -0
  367. painapple_code/static/js/widgets/plugins-widget.js +305 -0
  368. painapple_code/static/js/widgets/prompt-explorer-widget.js +1119 -0
  369. painapple_code/static/js/widgets/search-files-widget.js +529 -0
  370. painapple_code/static/js/widgets/skills-widget.js +843 -0
  371. painapple_code/static/js/widgets/snippets-widget.js +479 -0
  372. painapple_code/static/js/widgets/sub-agents-widget.js +333 -0
  373. painapple_code/static/js/widgets/tasks-widget.js +379 -0
  374. painapple_code/static/js/widgets/terminal/connection.js +283 -0
  375. painapple_code/static/js/widgets/terminal/gestures.js +357 -0
  376. painapple_code/static/js/widgets/terminal/init.js +642 -0
  377. painapple_code/static/js/widgets/terminal/link-providers.js +532 -0
  378. painapple_code/static/js/widgets/terminal/osc52.js +213 -0
  379. painapple_code/static/js/widgets/terminal/render.js +159 -0
  380. painapple_code/static/js/widgets/terminal/size.js +54 -0
  381. painapple_code/static/js/widgets/terminal/state.js +211 -0
  382. painapple_code/static/js/widgets/terminal/wrap-utils.js +101 -0
  383. painapple_code/static/js/widgets/terminal-widget.js +793 -0
  384. painapple_code/static/js/widgets/uploads-widget.js +461 -0
  385. painapple_code/static/js/widgets/zen-widget.js +1001 -0
  386. painapple_code/static/login.html +51 -0
  387. painapple_code/static/manifest.json +73 -0
  388. painapple_code/static/sw.js +293 -0
  389. painapple_code/static/vendor/LICENSES.txt +769 -0
  390. painapple_code/static/vendor/README.md +103 -0
  391. painapple_code/static/vendor/codemirror.js +35 -0
  392. painapple_code/static/vendor/codemirror.js.LEGAL.txt +304 -0
  393. painapple_code/static/vendor/github-markdown-dark.min.css +1124 -0
  394. painapple_code/static/vendor/highlight-github-dark.min.css +10 -0
  395. painapple_code/static/vendor/highlight-lang-dockerfile.min.js +8 -0
  396. painapple_code/static/vendor/highlight-lang-nginx.min.js +21 -0
  397. painapple_code/static/vendor/highlight-lang-properties.min.js +10 -0
  398. painapple_code/static/vendor/highlight-lang-scala.min.js +28 -0
  399. painapple_code/static/vendor/highlight.min.js +1213 -0
  400. painapple_code/static/vendor/marked.min.js +6 -0
  401. painapple_code/static/vendor/purify.min.js +3 -0
  402. painapple_code/static/vendor/xterm-addon-fit.js +2 -0
  403. painapple_code/static/vendor/xterm.css +209 -0
  404. painapple_code/static/vendor/xterm.js +2 -0
  405. painapple_code/static/web-client.html +635 -0
  406. painapple_code/subprocess_registry.py +535 -0
  407. painapple_code/tls_cert.py +79 -0
  408. painapple_code/tools/agents/shadow-git-helper.md +400 -0
  409. painapple_code/tools/excalidraw-to-svg.js +143 -0
  410. painapple_code/tools/install-helpers.sh +148 -0
  411. painapple_code/tools/shadow-git +320 -0
  412. painapple_code/tools/shadow-query +120 -0
  413. painapple_code/tools/vegalite-to-svg.js +79 -0
  414. painapple_code/turn_query.py +766 -0
  415. painapple_code/turn_tracker.py +221 -0
  416. painapple_code/utils/__init__.py +0 -0
  417. painapple_code/utils/agent_cli.py +306 -0
  418. painapple_code/utils/chart.py +103 -0
  419. painapple_code/utils/excalidraw.py +107 -0
  420. painapple_code/utils/file_lock.py +162 -0
  421. painapple_code/utils/file_paths.py +648 -0
  422. painapple_code/utils/generate_icons.py +251 -0
  423. painapple_code/utils/proc.py +205 -0
  424. painapple_code/utils/pty_backend.py +59 -0
  425. painapple_code/utils/pty_posix.py +160 -0
  426. painapple_code/utils/pty_windows.py +245 -0
  427. painapple_code/utils/token_profiles.py +91 -0
  428. painapple_code/viewer_templates.py +657 -0
  429. painapple_code/welcome_search.py +879 -0
  430. painapple_code-1.0.0.dist-info/METADATA +292 -0
  431. painapple_code-1.0.0.dist-info/RECORD +436 -0
  432. painapple_code-1.0.0.dist-info/WHEEL +5 -0
  433. painapple_code-1.0.0.dist-info/entry_points.txt +2 -0
  434. painapple_code-1.0.0.dist-info/licenses/LICENSE +661 -0
  435. painapple_code-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +261 -0
  436. painapple_code-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,572 @@
1
+ """The fleet view — every instance on this machine, local + docker.
2
+
3
+ Reached as ``painapple list`` (and its aliases) or as a bare
4
+ ``painapple status``; ``painapple status NAME`` is the per-deployment
5
+ detail block in manage_cmd. One renderer, two entry points.
6
+
7
+ Two sections, and the split is the point:
8
+
9
+ * **Deployments** — the things the verbs target: the flag-less root
10
+ deployment (``default``) plus every saved profile, each matched to a
11
+ running process (host) or container (docker).
12
+ * **Unmanaged processes** — painapple servers in the process table that
13
+ belong to NO deployment: launched by hand or by a service unit with
14
+ their own flags. Their name is just their ``--instance-name`` label.
15
+
16
+ Local servers have no registry — a bare ``painapple [serve]`` process IS
17
+ the instance — so they're discovered by scanning the process table and
18
+ parsing the serve flags out of each command line.
19
+ """
20
+
21
+ import os
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ from painapple_code.cli.ui import BOLD, DIM, GREEN, RESET, say
27
+
28
+
29
+ # ──── Local server discovery (process scan) ──────────────────────────────
30
+
31
+ def _iter_processes():
32
+ """(pid, argv_tokens) for every visible process.
33
+
34
+ psutil rather than `ps ax`: there is no ps on Windows, and psutil
35
+ hands back a real argv LIST instead of a line we have to re-split on
36
+ spaces — which is what made `C:\\Program Files\\...\\painapple.exe`
37
+ (or any macOS path with a space) unparseable.
38
+ """
39
+ import psutil
40
+ for proc in psutil.process_iter(["pid", "cmdline"]):
41
+ try:
42
+ argv = proc.info["cmdline"]
43
+ if argv:
44
+ yield proc.info["pid"], list(argv)
45
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
46
+ continue
47
+
48
+
49
+ def _proc_environ(pid):
50
+ """A process's environment as a dict, or None when unreadable.
51
+
52
+ Works for same-user processes on Linux, macOS AND Windows — the
53
+ /proc/<pid>/environ reads this replaces were Linux-only, which is why
54
+ macOS silently fell back to port-only matching (and why `painapple
55
+ stop` on Windows could kill whatever else held the port).
56
+ """
57
+ import psutil
58
+ try:
59
+ return psutil.Process(pid).environ()
60
+ except Exception:
61
+ # NoSuchProcess / AccessDenied / NotImplementedError — all mean
62
+ # "not ours to read", which callers already handle.
63
+ return None
64
+
65
+
66
+ def _basename(tok):
67
+ """Last path component, splitting on BOTH separators.
68
+
69
+ `Path(tok).name` only knows the running platform's separator, so a
70
+ Windows argv0 read on any other host (and, more usefully, in a test)
71
+ comes back whole. Splitting on both keeps the argv-shape checks below
72
+ platform-independent.
73
+ """
74
+ return re.split(r"[\\/]", tok)[-1]
75
+
76
+
77
+ def _is_python(tok):
78
+ """Does this argv[0] name a Python interpreter? Case-INSENSITIVE on
79
+ purpose: a Homebrew/python.org framework build's binary is literally
80
+ named `Python` (…/Python.framework/…/Python.app/Contents/MacOS/Python),
81
+ and that's the shebang interpreter pipx bakes into the console script
82
+ on macOS. A case-sensitive test made every pipx-installed `painapple`
83
+ on macOS invisible to the scan — `painapple list` reported the
84
+ deployment stopped while the server was serving."""
85
+ return _basename(tok).lower().startswith("python")
86
+
87
+
88
+ def _script_name(tok):
89
+ """argv[0]'s bare command name, minus a Windows executable suffix."""
90
+ name = _basename(tok).lower()
91
+ for suffix in (".exe", ".cmd", ".bat"):
92
+ if name.endswith(suffix):
93
+ return name[: -len(suffix)]
94
+ return name
95
+
96
+
97
+ def _serve_args(tokens):
98
+ """If `tokens` (an argv list) is a painapple SERVER process, return
99
+ its flag tail; None for anything else (other programs, subcommand
100
+ invocations like `painapple start work`, this very `painapple list`).
101
+
102
+ argv list only — the ps-based string callers are gone, and re-splitting
103
+ a joined command on spaces is precisely what broke paths containing
104
+ one (`C:\\Program Files\\…\\painapple.exe`).
105
+ """
106
+ rest = None
107
+ for i, tok in enumerate(tokens):
108
+ # `python [-u …] -m painapple_code` — the interpreter must be
109
+ # argv[0]. Without that anchor, a WRAPPER whose argument text
110
+ # merely contains the string (`sh -c 'python -m painapple_code
111
+ # --port …'`, an editor, a pager) ghosts in as a second row for
112
+ # the same instance.
113
+ if (tok == "painapple_code" and i >= 2 and tokens[i - 1] == "-m"
114
+ and _is_python(tokens[0])
115
+ and all(t.startswith("-") for t in tokens[1:i - 1])):
116
+ rest = tokens[i + 1:]
117
+ break
118
+ # Console script — as argv[0] or behind its shebang interpreter,
119
+ # possibly with interpreter options (`/path/python -E /path/bin/
120
+ # painapple --port …`). The interpreter guard keeps e.g.
121
+ # `grep foo …/painapple-code` from matching.
122
+ if (_script_name(tok) in ("painapple", "painapple-code")
123
+ and (i == 0
124
+ or (_is_python(tokens[0])
125
+ and all(t.startswith("-") for t in tokens[1:i])))):
126
+ rest = tokens[i + 1:]
127
+ break
128
+ if rest is None:
129
+ return None
130
+ # The server parser is flags-only; a leading non-flag is a subcommand.
131
+ if rest and not rest[0].startswith("-"):
132
+ if rest[0] != "serve":
133
+ return None
134
+ rest = rest[1:]
135
+ return rest
136
+
137
+
138
+ def _flag(rest, *names, default=None):
139
+ for i, tok in enumerate(rest):
140
+ for name in names:
141
+ if tok == name and i + 1 < len(rest):
142
+ return rest[i + 1]
143
+ if tok.startswith(name + "="):
144
+ return tok.split("=", 1)[1]
145
+ return default
146
+
147
+
148
+ def _in_container(pid):
149
+ """Linux: rootless-podman containers show up in the host's process
150
+ table — filter them out (they're the docker section's rows). macOS
151
+ containers live in a VM and never appear, so no check needed.
152
+
153
+ Environment markers first: our own image sets PAINAPPLE_IN_CONTAINER,
154
+ podman sets `container=podman`. The /proc/<pid>/root check is the
155
+ fallback — under --userns=keep-id that link reads back as '/' from
156
+ the host, which used to list every containerized server twice (once
157
+ as a phantom ad-hoc process on the container's own port)."""
158
+ env = _proc_environ(pid)
159
+ if env and ("PAINAPPLE_IN_CONTAINER" in env or "container" in env):
160
+ return True
161
+ if not sys.platform.startswith("linux"):
162
+ return False
163
+ try:
164
+ return os.readlink(f"/proc/{pid}/root") != "/"
165
+ except OSError:
166
+ return False # other user's process — can't tell, assume host
167
+
168
+
169
+ def _profile_of(rest, env):
170
+ """The profile name this process was launched under, '' when none.
171
+
172
+ argv FIRST, because `profiles.activate()` repoints the data home
173
+ IN-PROCESS: a foreground `painapple --profile NAME` is exec'd with
174
+ neither PAINAPPLE_CODE_HOME nor PAINAPPLE_PROFILE in its environment,
175
+ and psutil hands back the environment it was exec'd with — so the
176
+ only surviving evidence is the flag itself. Without this the instance
177
+ reads back as the ROOT deployment (port 8765, root serve.yaml): its
178
+ own profile row shows `stopped` while the process it can't see holds
179
+ the port, and `painapple stop` aims at it as if it were `default`.
180
+ The env var is the fallback for a spawner that exported it
181
+ (`painapple start NAME`, a service unit)."""
182
+ name = _flag(rest, "--profile", default="")
183
+ if not name:
184
+ name = (env or {}).get("PAINAPPLE_PROFILE", "")
185
+ return name.strip()
186
+
187
+
188
+ def _home_of(pid, rest=()):
189
+ """This process's data home as a resolved path string, or '' when the
190
+ environment isn't readable (another user). Readable-but-unset
191
+ resolves to the default home, so it can be matched against a profile
192
+ home; a `--profile NAME` launch resolves to that profile's home by
193
+ the same rule `activate()` applies."""
194
+ env = _proc_environ(pid)
195
+ if env is None:
196
+ return ""
197
+ raw = env.get("PAINAPPLE_CODE_HOME", "")
198
+ base = Path(raw or "~/.painapple-code").expanduser() # unset → default
199
+ name = _profile_of(rest, env)
200
+ if name:
201
+ from painapple_code.cli.profiles import resolve_home
202
+ base = resolve_home(name, base)
203
+ return str(base.resolve())
204
+
205
+
206
+ def _saved_defaults(home, cache):
207
+ """The saved serve values the process at data home `home` booted
208
+ with. '' (environment unreadable — another user) falls back to our
209
+ own view."""
210
+ if home not in cache:
211
+ from painapple_code.cli.serve_config import load
212
+ base = Path(home or os.environ.get("PAINAPPLE_CODE_HOME")
213
+ or "~/.painapple-code").expanduser()
214
+ # A PROFILE home keeps its serve values in profile.yaml — there is
215
+ # no serve.yaml beside it. Without this a profile instance reads
216
+ # back as built-in defaults (port 8765, cwd as workspace).
217
+ path = base / "profile.yaml"
218
+ if not path.is_file():
219
+ path = base / "serve.yaml"
220
+ cache[home] = load(path)[0]
221
+ return cache[home]
222
+
223
+
224
+ def _workspace_of(pid, rest, saved):
225
+ ws = _flag(rest, "--workspace", "--cwd")
226
+ if ws and ws != ".":
227
+ return ws
228
+ # No flag → the saved default (serve.yaml) if any, else the
229
+ # process's cwd — readable on Linux for same-user processes.
230
+ if saved.get("workspace"):
231
+ return saved["workspace"]
232
+ import psutil
233
+ try:
234
+ return psutil.Process(pid).cwd()
235
+ except Exception:
236
+ return ws or ""
237
+
238
+
239
+ def local_servers():
240
+ """[{pid, host, port, name, workspace}] for every running local
241
+ (non-container) painapple server process. Flag-less values fall back
242
+ to the saved serve defaults (each process's own serve.yaml), then
243
+ the built-ins — the same layering the server itself applies."""
244
+ me = os.getpid()
245
+ rows = []
246
+ cache = {}
247
+ for pid, argv in _iter_processes():
248
+ if pid == me:
249
+ continue
250
+ rest = _serve_args(argv)
251
+ if rest is None or _in_container(pid):
252
+ continue
253
+ home = _home_of(pid, rest)
254
+ saved = _saved_defaults(home, cache)
255
+ name = _flag(rest, "--instance-name",
256
+ default=saved.get("instance_name", ""))
257
+ if not name:
258
+ # Mirror the server's own defaulting: a profile instance
259
+ # without an explicit label uses its profile name.
260
+ from painapple_code.cli.serve_config import (
261
+ INSTANCE_NAME_MAX, PROFILE_PARENT_DIRS)
262
+ p = Path(home)
263
+ if p.parent.name in PROFILE_PARENT_DIRS:
264
+ name = p.name[:INSTANCE_NAME_MAX]
265
+ rows.append({
266
+ "pid": pid,
267
+ "host": _flag(rest, "--host", default=saved.get("host", "127.0.0.1")),
268
+ "port": _flag(rest, "--port", default=str(saved.get("port", 8765))),
269
+ "name": name,
270
+ "workspace": _workspace_of(pid, rest, saved),
271
+ "home": home,
272
+ # The exact token list — respawn and flag lookups read this.
273
+ # `command` is the joined form, for display only: never split
274
+ # it back apart (paths with spaces).
275
+ "argv": list(argv),
276
+ "command": " ".join(argv),
277
+ })
278
+ rows = _collapse_chains(rows)
279
+ rows.sort(key=lambda r: int(r["port"]) if r["port"].isdigit() else 0)
280
+ return rows
281
+
282
+
283
+ def _matched_ancestor(pid, by_pid):
284
+ """Nearest ancestor of `pid` that is itself a matched server row and
285
+ belongs to the SAME server, or None. Never raises."""
286
+ try:
287
+ import psutil
288
+ seen = {pid}
289
+ cur = psutil.Process(pid)
290
+ while True:
291
+ ppid = cur.ppid()
292
+ if not ppid or ppid in seen:
293
+ return None
294
+ if ppid in by_pid:
295
+ return ppid
296
+ seen.add(ppid)
297
+ cur = psutil.Process(ppid)
298
+ except Exception:
299
+ return None
300
+
301
+
302
+ def _collapse_chains(rows):
303
+ """One row per server, even when the server is a process chain.
304
+
305
+ A pipx console script on Windows is three processes — painapple.exe
306
+ launcher, the python.exe it starts, and uvicorn's reload child — all
307
+ carrying identical argv, so all three match and one server presented
308
+ as three rows. That put phantom entries in `painapple list`, and made
309
+ `stop` a coin flip: _kill terminates a single pid, so picking the
310
+ reload child left the supervisor to respawn it while picking the
311
+ launcher left the real server serving. Keep the topmost ancestor,
312
+ whose termination takes the rest of its own chain with it.
313
+
314
+ Only collapses within one server: the ancestor must agree on port and
315
+ data home. Running a second server from a terminal *inside* the first
316
+ is a genuinely separate deployment and keeps its own row.
317
+ """
318
+ by_pid = {r["pid"]: r for r in rows}
319
+ parent = {}
320
+ for r in rows:
321
+ anc = _matched_ancestor(r["pid"], by_pid)
322
+ if anc is not None:
323
+ a = by_pid[anc]
324
+ if a["port"] == r["port"] and a.get("home") == r.get("home"):
325
+ parent[r["pid"]] = anc
326
+
327
+ def root_of(pid):
328
+ seen = set()
329
+ while pid in parent and pid not in seen:
330
+ seen.add(pid)
331
+ pid = parent[pid]
332
+ return pid
333
+
334
+ def depth(pid):
335
+ d, seen = 0, set()
336
+ while pid in parent and pid not in seen:
337
+ seen.add(pid)
338
+ pid = parent[pid]
339
+ d += 1
340
+ return d
341
+
342
+ kept = []
343
+ for r in rows:
344
+ if r["pid"] in parent:
345
+ continue
346
+ # The launcher shim's cwd is %TEMP%, not the project — so when the
347
+ # workspace fell back to cwd, take it from the DEEPEST process in
348
+ # the chain, which is the one actually serving. Explicit --workspace
349
+ # flags are identical across the chain (same argv), so this only
350
+ # ever changes the fallback case.
351
+ chain = [c for c in rows if root_of(c["pid"]) == r["pid"]]
352
+ if len(chain) > 1:
353
+ deepest = max(chain, key=lambda c: depth(c["pid"]))
354
+ if deepest["workspace"]:
355
+ r = {**r, "workspace": deepest["workspace"]}
356
+ kept.append(r)
357
+ return kept
358
+
359
+
360
+ def match_process(running, home, port, exclude=()):
361
+ """PID of the running server that belongs to the host deployment at
362
+ `home` listening on `port`, or None.
363
+
364
+ Order matters — a data home can legitimately hold more than one
365
+ process (someone started a second server out of it with --port), and
366
+ the deployment's OWN row is the one whose port also matches:
367
+
368
+ 1. same data home AND same port — unambiguous
369
+ 2. same data home — a port override of that deployment
370
+ 3. UNKNOWN home, same port — macOS / other-user rows, where the
371
+ home can't be read at all. Restricted to unknown-home rows on
372
+ purpose: a row with a known, DIFFERENT home is a different
373
+ deployment that happens to sit on this port and must never be
374
+ claimed (or, in lifecycle_cmd, stopped) in its place.
375
+ """
376
+ home_res = str(Path(home).expanduser().resolve())
377
+ port = str(port)
378
+ rows = [r for r in running if r["pid"] not in exclude]
379
+ for test in (lambda r: r.get("home") == home_res and r["port"] == port,
380
+ lambda r: r.get("home") == home_res,
381
+ lambda r: not r.get("home") and r["port"] == port):
382
+ pid = next((r["pid"] for r in rows if test(r)), None)
383
+ if pid is not None:
384
+ return pid
385
+ return None
386
+
387
+
388
+ def _host_detail(pid, running, name):
389
+ """The parenthetical for a matched host row: the pid, plus the
390
+ running process's own --instance-name when it differs from the row
391
+ name (the root deployment is very often labelled something else)."""
392
+ if not pid:
393
+ return ""
394
+ hit = next((r for r in running if r["pid"] == pid), {})
395
+ label = hit.get("name") or ""
396
+ return f"pid {pid} · {label}" if label and label != name else f"pid {pid}"
397
+
398
+
399
+ def root_row(running, exclude=()):
400
+ """The flag-less root deployment as a deployment row — the target of
401
+ every verb called without a NAME. Always present: it's what a bare
402
+ `painapple` runs, whether or not it has ever been started."""
403
+ from painapple_code.cli.serve_config import load, root_home
404
+ home = root_home()
405
+ vals, _ = load(home / "serve.yaml")
406
+ port = str(vals.get("port", 8765))
407
+ pid = match_process(running, home, port, exclude)
408
+ hit = next((r for r in running if r["pid"] == pid), None) if pid else None
409
+ return {"name": "default", "mode": "host", "pid": pid,
410
+ "host": vals.get("host", "127.0.0.1"), "port": port,
411
+ "home": str(home),
412
+ # No saved workspace at the root — a bare `painapple` serves
413
+ # whatever directory it's launched from.
414
+ "workspace": (hit or {}).get("workspace") or "(cwd at launch)",
415
+ "detail": _host_detail(pid, running, "default")}
416
+
417
+
418
+ def deployment_rows(running):
419
+ """Every named target, root deployment first: [{name, mode, pid,
420
+ host, port, workspace, detail}]. Profiles are matched before the
421
+ root row so a profile never loses its process to it."""
422
+ rows = profile_rows(running)
423
+ claimed = {r["pid"] for r in rows if r.get("pid")}
424
+ return [root_row(running, claimed), *rows]
425
+
426
+
427
+ def profile_rows(running):
428
+ """[{name, mode, host, port, workspace, state}] for every saved
429
+ profile (profiles/NAME/profile.yaml). Host profiles match a running
430
+ local server by data home (exact, via psutil environ()) else by port;
431
+ docker profiles ask the container runtime."""
432
+ import shutil
433
+ from painapple_code.cli import profiles as profmod
434
+
435
+ probe = bool(shutil.which("docker") or shutil.which("podman"))
436
+ rows = []
437
+ for name in profmod.list_profiles():
438
+ prof = profmod.load(name)
439
+ if prof is None:
440
+ continue
441
+ if prof.is_docker:
442
+ cfg = prof.docker_settings()
443
+ status = None
444
+ if probe or cfg.runtime: # avoid auto_detect()'s die()
445
+ try:
446
+ from painapple_code.cli.deploy.runtime import Runtime
447
+ status = Runtime(cfg).container_status(cfg.container)
448
+ except (SystemExit, OSError):
449
+ status = None
450
+ ws = (f"{len(cfg.workspaces)} workspaces"
451
+ if cfg.workspace_mode == "multi"
452
+ else cfg.workspace or "(no workspace)")
453
+ rows.append({"name": name, "mode": "docker", "pid": None,
454
+ "host": cfg.listen_host, "port": str(cfg.port),
455
+ "home": "", "workspace": ws, "detail": status or ""})
456
+ continue
457
+ vals = prof.host_values()
458
+ port = str(vals.get("port", 8765))
459
+ home = prof.home()
460
+ pid = match_process(running, home, port)
461
+ rows.append({"name": name, "mode": "host", "pid": pid,
462
+ "host": vals.get("host", "127.0.0.1"), "port": port,
463
+ "home": str(home),
464
+ "workspace": vals.get("workspace")
465
+ or vals.get("instance_name") or "",
466
+ "detail": _host_detail(pid, running, name)})
467
+ return rows
468
+
469
+
470
+ def adhoc_container_row():
471
+ """The flag-less ``painapple --in-docker`` sandbox (container
472
+ 'painapple-code'), when the runtime knows it. None otherwise."""
473
+ import shutil
474
+ if not (shutil.which("docker") or shutil.which("podman")):
475
+ return None
476
+ try:
477
+ from painapple_code.cli.deploy.config import DockerSettings
478
+ from painapple_code.cli.deploy.runtime import Runtime
479
+ cfg = DockerSettings(profile=None)
480
+ rt = Runtime(cfg)
481
+ if not rt.container_exists(cfg.container):
482
+ return None
483
+ status = rt.container_status(cfg.container)
484
+ return {"name": cfg.container, "host": cfg.listen_host,
485
+ "port": str(cfg.port), "detail": status or ""}
486
+ except (SystemExit, OSError):
487
+ return None
488
+
489
+
490
+ # ──── Command ─────────────────────────────────────────────────────────────
491
+
492
+ def _state(detail):
493
+ return (f"{GREEN}running{RESET} {DIM}({detail}){RESET}"
494
+ if detail else f"{DIM}stopped{RESET}")
495
+
496
+
497
+ def main(argv=None):
498
+ from painapple_code.cli import profiles as profmod
499
+ profmod.ensure_migrated()
500
+
501
+ say(f"{BOLD}Painapple Code instances{RESET}")
502
+ say()
503
+
504
+ locals_ = local_servers()
505
+ rows = deployment_rows(locals_)
506
+ claimed = {r["pid"] for r in rows if r.get("pid")}
507
+ # Servers in the process table that belong to no deployment: started
508
+ # directly (by hand, a service unit) with their own flags, so their
509
+ # name is just their --instance-name and no NAME verb targets them.
510
+ unmanaged = [r for r in locals_ if r["pid"] not in claimed]
511
+ saved = [r for r in rows if r["name"] != "default"]
512
+
513
+ say(f"{BOLD}Deployments{RESET} {DIM}(named targets — "
514
+ f"painapple start/stop/status NAME){RESET}")
515
+ width = max(len(r["name"]) for r in rows)
516
+ mwidth = max(len(r["mode"]) for r in rows) + 2 # brackets
517
+ for r in rows:
518
+ badge = f"[{r['mode']}]"
519
+ say((f" {BOLD}{r['name']:<{width}}{RESET} "
520
+ f"{DIM}{badge}{RESET}{'':<{mwidth - len(badge)}}"
521
+ f" {r['host']}:{r['port']} {_state(r['detail'])}"
522
+ f" {DIM}{r['workspace']}{RESET}").rstrip())
523
+ if not saved:
524
+ say(f" {DIM}default = what a bare `painapple` runs; add named ones "
525
+ f"with: painapple setup NAME{RESET}")
526
+
527
+ if unmanaged:
528
+ say()
529
+ say(f"{BOLD}Unmanaged processes{RESET} {DIM}(started directly — "
530
+ f"not from a saved deployment){RESET}")
531
+ width = max(len(r["name"] or "—") for r in unmanaged)
532
+ for r in unmanaged:
533
+ say((f" {BOLD}{r['name'] or '—':<{width}}{RESET}"
534
+ f" {r['host']}:{r['port']}"
535
+ f" {GREEN}running{RESET} {DIM}(pid {r['pid']}){RESET}"
536
+ f" {DIM}{r['workspace']}{RESET}").rstrip())
537
+ say(f" {DIM}their name is just the --instance-name label; "
538
+ f"stop/restart/status take it too{RESET}")
539
+
540
+ adhoc = adhoc_container_row()
541
+ if adhoc:
542
+ say()
543
+ say(f"{BOLD}Ad-hoc container{RESET} {DIM}(painapple --in-docker){RESET}")
544
+ say(f" {BOLD}{adhoc['name']}{RESET} {adhoc['host']}:{adhoc['port']}"
545
+ f" {_state(adhoc['detail'])}")
546
+
547
+ say()
548
+ say(f"{BOLD}Address{RESET} {DIM}= the host it binds to{RESET}")
549
+ say(f" {'0.0.0.0':<9} {DIM}all interfaces — reachable from the LAN{RESET}")
550
+ say(f" {'127.0.0.1':<9} {DIM}localhost only{RESET}")
551
+
552
+ say()
553
+ if saved:
554
+ # Past first run: one pointer, not a seven-line cheat sheet.
555
+ say(f"{DIM}Details for one:{RESET} painapple status NAME "
556
+ f"{DIM}· full help:{RESET} painapple help")
557
+ else:
558
+ say(f"{BOLD}Commands{RESET} {DIM}(NAME = a deployment above; omit it "
559
+ f"to target `default`){RESET}")
560
+ hints = [
561
+ ("painapple setup NAME", "create or reconfigure a profile"),
562
+ ("painapple start NAME", "run it in the background"),
563
+ ("painapple stop NAME", "stop it"),
564
+ ("painapple restart NAME", "stop, then start again"),
565
+ ("painapple status NAME", "running or not, address, workspace"),
566
+ ("painapple logs NAME", "tail its log"),
567
+ ("painapple password NAME", "print its login URL"),
568
+ ]
569
+ width = max(len(cmd) for cmd, _ in hints)
570
+ for cmd, desc in hints:
571
+ say(f" {cmd:<{width}} {DIM}{desc}{RESET}")
572
+ return 0