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,670 @@
1
+ """Management verbs — ``painapple status/logs/password/pull/shell/
2
+ extract/claude-login`` and the scripted ``painapple profile`` channel.
3
+
4
+ Every verb takes an optional NAME (positional or ``--profile``) and
5
+ dispatches on the target's mode:
6
+
7
+ * host — data home / process / server logs / global config file
8
+ * docker — container runtime (same bodies as `--in-docker`)
9
+
10
+ No NAME targets the root deployment: the global defaults for
11
+ status/logs/password, the ad-hoc ``--in-docker`` container for the
12
+ docker-only verbs (shell/extract/claude-login).
13
+
14
+ ``painapple profile`` is the non-interactive scripting surface (used
15
+ by the desktop launcher): get/set/path/list against profile.yaml with
16
+ the same validation the wizard applies.
17
+ """
18
+
19
+ import os
20
+ import sys
21
+ import time
22
+ from pathlib import Path
23
+
24
+ from painapple_code import paths
25
+ from painapple_code.cli import profiles, serve_config
26
+ from painapple_code.cli.netinfo import detect_local_ips
27
+ from painapple_code.cli.ui import (
28
+ BOLD, DIM, GREEN, RESET, die, err, ok, print_credentials, say, warn,
29
+ )
30
+
31
+ DOCKER_ONLY = ("shell", "extract", "claude-login")
32
+
33
+
34
+ def _resolve_target(argv, allow_positional=True):
35
+ """(name|None, rest): ``--profile NAME`` anywhere, or a leading
36
+ positional. '' / 'default' → None (root)."""
37
+ prof, rest = serve_config.extract_profile(list(argv or []))
38
+ if allow_positional and rest and not rest[0].startswith("-"):
39
+ if prof and prof != rest[0]:
40
+ raise ValueError(
41
+ f"Profile given twice: {rest[0]!r} and --profile {prof!r}")
42
+ prof, rest = rest[0], rest[1:]
43
+ if prof in ("", "default"):
44
+ prof = None
45
+ return prof, rest
46
+
47
+
48
+ def _named_root(argv):
49
+ """True when the caller explicitly typed the root deployment's name
50
+ (``painapple status default``). ``_resolve_target`` folds 'default'
51
+ into None, and status needs the distinction: a bare `status` is the
52
+ fleet view, `status default` is the root's own detail block."""
53
+ try:
54
+ prof, rest = serve_config.extract_profile(list(argv or []))
55
+ except ValueError:
56
+ return False
57
+ token = prof or (rest[0] if rest and not rest[0].startswith("-") else "")
58
+ return token == "default"
59
+
60
+
61
+ def _pop_in_docker(rest):
62
+ """(rest, wanted): strip ``--in-docker`` — the root deployment's mode
63
+ selector. A profile already carries `mode:`; only the nameless ad-hoc
64
+ sandbox needs to be asked for explicitly."""
65
+ if "--in-docker" in rest:
66
+ return [a for a in rest if a != "--in-docker"], True
67
+ return rest, False
68
+
69
+
70
+ def _adhoc_container_running():
71
+ """True if the ad-hoc ``--in-docker`` container is up. Fails soft —
72
+ no runtime, no image, an unreachable daemon all just mean 'no'
73
+ (auto_detect die()s, hence SystemExit in the catch)."""
74
+ try:
75
+ from painapple_code.cli.deploy.runtime import Runtime
76
+ cfg = _root_docker_settings()
77
+ return Runtime(cfg).container_running(cfg.container)
78
+ except (Exception, SystemExit):
79
+ return False
80
+
81
+
82
+ def _root_docker_settings():
83
+ """DockerSettings for the root/ad-hoc deployment: package defaults +
84
+ the global serve.yaml runtime/image/network keys."""
85
+ from painapple_code.cli.deploy.config import ConfigError, DockerSettings
86
+ cfg = DockerSettings(profile=None)
87
+ values, _ = serve_config.load(serve_config.root_home() / "serve.yaml")
88
+ for key, cfg_key in (("runtime", "RUNTIME"), ("runtime_flags", "RUNTIME_FLAGS"),
89
+ ("image", "IMAGE"), ("host", "LISTEN_HOST"),
90
+ ("port", "PORT"), ("tls", "TLS_MODE")):
91
+ if values.get(key) not in (None, ""):
92
+ try:
93
+ cfg.assign(cfg_key, str(values[key]))
94
+ except ConfigError:
95
+ pass
96
+ return cfg
97
+
98
+
99
+ def _docker_settings_for(prof):
100
+ if prof is None:
101
+ return _root_docker_settings()
102
+ from painapple_code.cli.deploy.launch import docker_settings_with_globals
103
+ return docker_settings_with_globals(prof)
104
+
105
+
106
+ # ──── Host-mode helpers ──────────────────────────────────────────────────
107
+
108
+ def _auth_config_path():
109
+ """Where the server's auth config lives — resolved exactly as the server does.
110
+
111
+ Must track ``init_auth_state``'s default (paths.CONFIG_HOME), not a
112
+ hand-rolled XDG lookup: the server WRITES this file, so the reader has to
113
+ follow the writer. It previously honored XDG_CONFIG_HOME, which the server
114
+ ignores — so with that set, the server wrote ~/.config/painapple-code while
115
+ `painapple password` looked in $XDG_CONFIG_HOME/painapple-code and reported
116
+ "No password found. Has the server ever started?" against a running server.
117
+ """
118
+ return paths.CONFIG_HOME / "config.yaml"
119
+
120
+
121
+ def _host_config_value(key):
122
+ """Read one scalar out of the auth config without importing YAML."""
123
+ path = _auth_config_path()
124
+ prefix = f"{key}:"
125
+ try:
126
+ for line in path.read_text(encoding="utf-8").splitlines():
127
+ if line.startswith(prefix):
128
+ return line.split(":", 1)[1].strip()
129
+ except OSError:
130
+ pass
131
+ return ""
132
+
133
+
134
+ def _host_password_value():
135
+ return _host_config_value("password")
136
+
137
+
138
+ def _host_token_value():
139
+ """The derived api_token — what ?tkn= links and scripts use.
140
+
141
+ Empty on a config written by a pre-WP-02 build (or before the server has
142
+ started once); callers fall back to the bare URL rather than pasting the
143
+ password into a link.
144
+ """
145
+ return _host_config_value("api_token")
146
+
147
+
148
+ def _host_urls(vals):
149
+ host = vals.get("host", "127.0.0.1")
150
+ port = vals.get("port", 8765)
151
+ tls = vals.get("tls", "auto")
152
+ loopback = host in ("127.0.0.1", "::1", "localhost")
153
+ scheme = "https" if tls == "on" or (tls == "auto" and not loopback) else "http"
154
+ hosts = ([ip for ip, _ in detect_local_ips()] + ["127.0.0.1"]
155
+ if host == "0.0.0.0" else [host])
156
+ return [f"{scheme}://{h}:{port}" for h in hosts]
157
+
158
+
159
+ def _host_password(vals, label):
160
+ pw = _host_password_value()
161
+ if not pw:
162
+ start = f"painapple start {label}" if label else "painapple start"
163
+ die(f"No password found at {_auth_config_path()}. "
164
+ f"Has the server ever started? ({start})",
165
+ None if label else
166
+ f" {DIM}Running it containerized? painapple password --in-docker{RESET}")
167
+ token = _host_token_value()
168
+ urls = [f"{url}/?tkn={token}" if token else f"{url}/" for url in _host_urls(vals)]
169
+ print_credentials(urls, pw, token)
170
+ return 0
171
+
172
+
173
+ def _host_running_pid(name, home):
174
+ """PID of the running server for a host target (exact home match,
175
+ port fallback restricted to unknown-home rows)."""
176
+ from painapple_code.cli.lifecycle_cmd import _match, _resolve
177
+ from painapple_code.cli.list_cmd import local_servers
178
+ return _match(local_servers(), _resolve(name))
179
+
180
+
181
+ def _follow_file(path, last_lines=50, poll=0.5):
182
+ """`tail -n N -F` in pure Python.
183
+
184
+ Replaces os.execvp("tail", …): there's no tail on Windows and no
185
+ exec-replace semantics either (execvp there spawns a NEW process and
186
+ returns, so the parent would fall through to die() below while a
187
+ detached tail scribbled over the same console). -F semantics are kept
188
+ — pick up rotation and truncation.
189
+
190
+ The file is opened per poll rather than held open, which matters on
191
+ Windows: Python's open() doesn't request FILE_SHARE_DELETE, so a held
192
+ handle makes RotatingFileHandler's os.rename() fail with WinError 32.
193
+ The handler would then retry on EVERY subsequent record (the file is
194
+ still over maxBytes) and flood crash.log — i.e. running `painapple
195
+ logs` would break the server's logging for as long as it was open.
196
+ """
197
+ pos, ident = 0, None
198
+
199
+ def _identity(st):
200
+ # st_ctime means DIFFERENT things per platform, and only one of
201
+ # them is a rotation token:
202
+ # win32 — creation time. Stable across appends, and needed here
203
+ # because st_ino is 0 on some Windows filesystems.
204
+ # POSIX — inode CHANGE time, which bumps on EVERY append. Using
205
+ # it made a growing log look rotated on every 0.5s poll
206
+ # → pos = 0 → the whole (up to 10MB) server.log reprinted,
207
+ # over and over, for as long as the server kept logging.
208
+ # So it's win32-only; elsewhere the inode alone is the identity.
209
+ if sys.platform == "win32":
210
+ return (st.st_ino, st.st_ctime)
211
+ return (st.st_ino, st.st_dev)
212
+
213
+ try:
214
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
215
+ f.seek(0, os.SEEK_END)
216
+ size = f.tell()
217
+ f.seek(max(0, size - 64 * 1024))
218
+ for line in f.read().splitlines()[-last_lines:]:
219
+ print(line, flush=True)
220
+ pos = f.tell()
221
+ ident = _identity(os.stat(path))
222
+ except OSError:
223
+ pass
224
+
225
+ while True:
226
+ time.sleep(poll)
227
+ try:
228
+ st = os.stat(path)
229
+ except OSError:
230
+ continue # rotated away; wait for it back
231
+ new_ident = _identity(st)
232
+ if ident is not None and new_ident != ident:
233
+ pos, ident = 0, new_ident # replaced by rotation
234
+ elif st.st_size < pos:
235
+ pos = 0 # truncated in place
236
+ else:
237
+ ident = new_ident
238
+ if st.st_size == pos:
239
+ continue
240
+ try:
241
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
242
+ f.seek(pos)
243
+ chunk = f.read()
244
+ pos = f.tell()
245
+ except OSError:
246
+ continue
247
+ if chunk:
248
+ print(chunk, end="", flush=True)
249
+
250
+
251
+ def _host_logs(home):
252
+ logs = Path(home) / "logs"
253
+ for candidate in (logs / "server.log", logs / "console.log"):
254
+ if candidate.is_file():
255
+ say(f"{DIM}Following {candidate} — Ctrl-C to stop{RESET}")
256
+ try:
257
+ _follow_file(candidate)
258
+ except KeyboardInterrupt:
259
+ return 0
260
+ except OSError as e:
261
+ die(f"Cannot read {candidate}: {e}")
262
+ die(f"No logs yet under {logs} — has this deployment ever started?")
263
+
264
+
265
+ # ──── status ─────────────────────────────────────────────────────────────
266
+
267
+ def _host_status(name, prof):
268
+ home = prof.home() if prof else serve_config.home_root()
269
+ vals = prof.host_values() if prof else serve_config.load()[0]
270
+ label = name or "default"
271
+ say(f"{BOLD}{label}{RESET} {DIM}[host]{RESET}")
272
+ say(f" Data home : {home}")
273
+ say(f" Workspace : {vals.get('workspace') or DIM + '(current dir at start)' + RESET}")
274
+ say(f" Network : {vals.get('host', '127.0.0.1')}:{vals.get('port', 8765)}, "
275
+ f"TLS {vals.get('tls', 'auto')}")
276
+ if prof and (prof.data.get("instance_name") or prof.data.get("accent")):
277
+ say(f" Cosmetics : label {prof.data.get('instance_name') or ''} "
278
+ f"accent {prof.data.get('accent') or ''}".rstrip())
279
+ pid = _host_running_pid(name, home)
280
+ if pid:
281
+ ok(f"Running (pid {pid})")
282
+ for url in _host_urls(vals):
283
+ say(f" → {url}/")
284
+ else:
285
+ warn(f"Not running. Start with: painapple start {name or ''}".rstrip())
286
+ return 0
287
+
288
+
289
+ def _docker_status(name, cfg):
290
+ from painapple_code.cli.deploy.container import get_password, listen_scope
291
+ from painapple_code.cli.deploy.runtime import Runtime
292
+ rt = Runtime(cfg)
293
+ label = name or cfg.container
294
+ say(f"{BOLD}{label}{RESET} {DIM}[docker]{RESET}")
295
+ flags = f" {DIM}({cfg.runtime_flags}){RESET}" if cfg.runtime_flags else ""
296
+ say(f" Runtime : {rt.name}{flags}")
297
+ unset = f"{DIM}(unset — run: painapple setup {name or ''}){RESET}".replace(" )", ")")
298
+ if cfg.workspace_mode == "multi":
299
+ first = cfg.workspaces[0] if cfg.workspaces else unset
300
+ say(f" Workspaces : {first} {DIM}({cfg.workspace_mode}){RESET}")
301
+ for ws in cfg.workspaces[1:]:
302
+ say(f" {ws}")
303
+ else:
304
+ say(f" Workspace : {cfg.workspace or unset} {DIM}({cfg.workspace_mode}){RESET}")
305
+ say(f" .claude : {cfg.claude_home}")
306
+ say(f" .claude.json: {cfg.effective_claude_json()}")
307
+ say(f" Data : {cfg.data_volume} {DIM}({'bind' if cfg.data_is_bind() else 'volume'}){RESET}")
308
+ say(f" Config : {cfg.config_volume} {DIM}({'bind' if cfg.config_is_bind() else 'volume'}){RESET}")
309
+ say(f" Image : {cfg.image}")
310
+ say(f" Container : {cfg.container}")
311
+ say(f" Listen : {cfg.listen_host}:{cfg.port} {DIM}→ container :8765{RESET}")
312
+ say(f" {DIM}host bind — {listen_scope(cfg)}{RESET}")
313
+ say(f" TLS : {cfg.tls_mode}")
314
+ if cfg.instance_name:
315
+ say(f" Instance : {cfg.instance_name}")
316
+ if cfg.accent:
317
+ say(f" Accent : {cfg.accent}")
318
+ say()
319
+
320
+ pw = get_password(cfg, rt) if cfg.config_is_bind() else ""
321
+ status = rt.container_status(cfg.container)
322
+ if status:
323
+ scheme = "https" if cfg.effective_tls() == "on" else "http"
324
+ url_host = "127.0.0.1" if cfg.listen_host == "0.0.0.0" else cfg.listen_host
325
+ ok(f"Running ({status})")
326
+ say(f" → {scheme}://{url_host}:{cfg.port}/")
327
+ if pw:
328
+ say(f" Password (login form): {BOLD}{pw}{RESET}")
329
+ else:
330
+ warn(f"Container is not running. Start with: "
331
+ f"painapple start {name or ''}".rstrip()
332
+ if name else "Container is not running. Start with: painapple --in-docker")
333
+ if pw:
334
+ say(f" {DIM}Password (login form; reused on next start):{RESET} {BOLD}{pw}{RESET}")
335
+ return 0
336
+
337
+
338
+ def _process_status(row):
339
+ """Detail block for a running server that belongs to no deployment —
340
+ launched directly with its own flags, so its config isn't on disk
341
+ anywhere: everything shown here is read off the live process."""
342
+ label = row["name"] or f"pid {row['pid']}"
343
+ tls = _flag_of(row, "--tls") or "auto"
344
+ say(f"{BOLD}{label}{RESET} {DIM}[process]{RESET}")
345
+ say(f" Data home : {row.get('home') or DIM + '(not readable)' + RESET}")
346
+ say(f" Workspace : {row.get('workspace') or DIM + '(unknown)' + RESET}")
347
+ say(f" Network : {row['host']}:{row['port']}, TLS {tls}")
348
+ ok(f"Running (pid {row['pid']})")
349
+ for url in _host_urls({"host": row["host"], "port": row["port"], "tls": tls}):
350
+ say(f" → {url}/")
351
+ say(f" {DIM}Started directly — no saved deployment owns it. "
352
+ f"stop/restart/logs take this name;{RESET}")
353
+ say(f" {DIM}painapple setup {row['name'] or 'NAME'} would make it "
354
+ f"a profile.{RESET}")
355
+ return 0
356
+
357
+
358
+ def _flag_of(row, name):
359
+ # row['argv'] is the exact token list psutil handed back — re-splitting
360
+ # the joined display string on spaces would mangle any path with one.
361
+ from painapple_code.cli.list_cmd import _flag
362
+ return _flag(row.get("argv") or [], name)
363
+
364
+
365
+ def _status(name, rest, detail=False):
366
+ if name:
367
+ prof = profiles.load(name)
368
+ if prof is None:
369
+ err(f"No profile named {name!r}. See: painapple list")
370
+ return 1
371
+ for problem in prof.problems:
372
+ warn(f"profile.yaml: {problem}")
373
+ if prof.is_docker:
374
+ return _docker_status(name, _docker_settings_for(prof))
375
+ return _host_status(name, prof)
376
+
377
+ # `painapple status default` — the root deployment's own detail block.
378
+ if detail:
379
+ return _host_status(None, None)
380
+
381
+ # Bare `painapple status` IS the fleet view (same renderer as
382
+ # `painapple list`) — one overview, reachable by either name.
383
+ from painapple_code.cli.list_cmd import main as fleet_view
384
+ return fleet_view(rest)
385
+
386
+
387
+ # ──── profile (scripting: get/set/path/list) ─────────────────────────────
388
+
389
+ def _profile_usage():
390
+ say(f"""{BOLD}painapple profile{RESET} — scripted profile access (the wizard: painapple setup NAME)
391
+
392
+ painapple profile {GREEN}list{RESET} names, one per line
393
+ painapple profile {GREEN}get{RESET} NAME [KEY …] all keys (KEY=VALUE) or specific values
394
+ painapple profile {GREEN}set{RESET} NAME KEY=VALUE … validate + write (creates NAME; --mode host|docker)
395
+ painapple profile {GREEN}path{RESET} NAME profile.yaml location
396
+ painapple profile {GREEN}delete{RESET} NAME remove the profile config{DIM} (host data home / volumes stay){RESET}""")
397
+ return 1
398
+
399
+
400
+ def _profile_keys(prof):
401
+ """The (yaml_key → printable) ordering for get with no args."""
402
+ if prof.is_docker:
403
+ from painapple_code.cli.deploy.config import settings_to_data
404
+ data = settings_to_data(prof.docker_settings())
405
+ else:
406
+ data = {k: prof.data.get(k, "") for k in serve_config.HOST_KEYS}
407
+ return data
408
+
409
+
410
+ def _profile_cmd(argv):
411
+ sub = argv[0] if argv else ""
412
+ args = argv[1:]
413
+ if sub in ("", "help", "-h", "--help"):
414
+ return _profile_usage()
415
+
416
+ if sub in ("list", "ls"):
417
+ for name in profiles.list_profiles():
418
+ prof = profiles.load(name)
419
+ say(f"{name}\t{prof.mode if prof else '?'}")
420
+ return 0
421
+
422
+ if not args:
423
+ return _profile_usage()
424
+ name, rest = args[0], args[1:]
425
+
426
+ if sub == "path":
427
+ say(str(profiles.profile_path(name)))
428
+ return 0
429
+
430
+ if sub == "delete":
431
+ if not profiles.exists(name):
432
+ err(f"No profile named {name!r}")
433
+ return 1
434
+ prof = profiles.load(name)
435
+ profiles.profile_path(name).unlink()
436
+ if prof and prof.is_docker:
437
+ try:
438
+ profiles.profile_home(name).rmdir()
439
+ except OSError:
440
+ pass # host data or stray files — leave the dir
441
+ ok(f"Profile '{name}' removed (container/volume left as-is)")
442
+ else:
443
+ ok(f"Profile '{name}' removed (data home kept: "
444
+ f"{profiles.profile_home(name)})")
445
+ return 0
446
+
447
+ if sub == "get":
448
+ prof = profiles.load(name)
449
+ if prof is None:
450
+ err(f"No profile named {name!r}")
451
+ return 1
452
+ data = _profile_keys(prof)
453
+ if not rest:
454
+ say(f"mode={prof.mode}")
455
+ for key, value in data.items():
456
+ if key == "workspaces":
457
+ continue
458
+ say(f"{key}={value}")
459
+ if data.get("workspaces"):
460
+ say("workspaces=" + os.pathsep.join(data["workspaces"]))
461
+ return 0
462
+ for key in rest:
463
+ if key == "mode":
464
+ say(prof.mode)
465
+ elif key in data:
466
+ value = data[key]
467
+ say(os.pathsep.join(value) if isinstance(value, list) else str(value))
468
+ else:
469
+ err(f"Unknown key for a {prof.mode} profile: {key}")
470
+ return 1
471
+ return 0
472
+
473
+ if sub == "set":
474
+ return _profile_set(name, rest)
475
+
476
+ err(f"Unknown profile subcommand: {sub}")
477
+ return _profile_usage()
478
+
479
+
480
+ def _profile_set(name, args):
481
+ if not profiles.valid_name(name):
482
+ err(f"Bad profile name: {name!r}")
483
+ return 1
484
+ mode = None
485
+ pairs = []
486
+ i = 0
487
+ while i < len(args):
488
+ arg = args[i]
489
+ if arg in ("--mode",):
490
+ if i + 1 >= len(args):
491
+ die("Missing value for --mode")
492
+ mode, i = args[i + 1], i + 2
493
+ continue
494
+ if arg.startswith("--mode="):
495
+ mode, i = arg.split("=", 1)[1], i + 1
496
+ continue
497
+ if arg.startswith("--"):
498
+ if "=" in arg:
499
+ flag, value = arg.split("=", 1)
500
+ i += 1
501
+ else:
502
+ flag = arg
503
+ if i + 1 >= len(args):
504
+ die(f"Missing value for {flag}")
505
+ value = args[i + 1]
506
+ i += 2
507
+ pairs.append((flag[2:].replace("-", "_"), value))
508
+ continue
509
+ if "=" in arg:
510
+ key, value = arg.split("=", 1)
511
+ pairs.append((key, value))
512
+ i += 1
513
+ continue
514
+ die(f"Expected KEY=VALUE or --key value, got: {arg}")
515
+
516
+ prof = profiles.load(name)
517
+ if prof is None:
518
+ # Scripted creation (the desktop launcher's path) — docker by
519
+ # default, that's what non-interactive callers manage.
520
+ prof = profiles.Profile(name, mode or "docker", {})
521
+ elif mode and mode != prof.mode:
522
+ if mode not in profiles.MODES:
523
+ die(f"Bad mode: {mode!r} (host or docker)")
524
+ prof = profiles.Profile(name, mode, prof.data)
525
+
526
+ if prof.is_docker:
527
+ from painapple_code.cli.deploy.config import (
528
+ ConfigError, settings_to_data)
529
+ cfg = prof.docker_settings()
530
+ for key, value in pairs:
531
+ upper = key.upper()
532
+ upper = {"HOST": "LISTEN_HOST", "TLS": "TLS_MODE"}.get(upper, upper)
533
+ try:
534
+ cfg.assign(upper, value)
535
+ except ConfigError as e:
536
+ err(str(e))
537
+ return 1
538
+ ok(f"{key} = {cfg.get(upper)}")
539
+ data = settings_to_data(cfg)
540
+ else:
541
+ data = dict(prof.data)
542
+ for key, value in pairs:
543
+ # Accept the docker half's uppercase spelling too, so one
544
+ # vocabulary drives both modes for scripted callers.
545
+ key = {"listen_host": "host", "tls_mode": "tls"}.get(
546
+ key.lower(), key.lower())
547
+ if key not in serve_config.HOST_KEYS:
548
+ err(f"Unknown key for a host profile: {key}")
549
+ return 1
550
+ try:
551
+ data[key] = serve_config._check(key, value)
552
+ except ValueError as e:
553
+ err(str(e))
554
+ return 1
555
+ ok(f"{key} = {data[key]}")
556
+ path = profiles.save(name, prof.mode, data)
557
+ ok(f"Saved to {path}")
558
+ return 0
559
+
560
+
561
+ # ──── Entry ───────────────────────────────────────────────────────────────
562
+
563
+ def main(verb, argv):
564
+ profiles.ensure_migrated()
565
+
566
+ try:
567
+ if verb == "profile":
568
+ return _profile_cmd(list(argv or []))
569
+
570
+ if verb == "pull":
571
+ # Positionals are the TAG here; --profile targets a profile's
572
+ # image (rarely needed — image tags are usually global).
573
+ name, rest = _resolve_target(argv, allow_positional=False)
574
+ prof = profiles.load(name) if name else None
575
+ if name and prof is None:
576
+ err(f"No profile named {name!r}. See: painapple list")
577
+ return 1
578
+ from painapple_code.cli.deploy.container import cmd_pull
579
+ return cmd_pull(_docker_settings_for(prof), rest) or 0
580
+
581
+ name, rest = _resolve_target(argv)
582
+ prof = profiles.load(name) if name else None
583
+ if name and prof is None:
584
+ # Not a saved profile — but `painapple list` also shows
585
+ # unmanaged processes, and start/stop/restart already target
586
+ # those by label/pid/port. status/logs follow suit rather
587
+ # than claiming a thing on screen doesn't exist.
588
+ row = None
589
+ if verb in ("status", "logs"):
590
+ from painapple_code.cli.lifecycle_cmd import _match_running
591
+ row, problem = _match_running(name)
592
+ if problem:
593
+ err(problem)
594
+ return 1
595
+ if row is None:
596
+ err(f"No profile named {name!r}. See: painapple list")
597
+ if profiles.valid_name(name):
598
+ say(f" {DIM}Create it with:{RESET} painapple setup {name}")
599
+ return 1
600
+ if verb == "status":
601
+ return _process_status(row)
602
+ _host_logs(row.get("home") or serve_config.home_root())
603
+ return 0
604
+
605
+ if verb == "status":
606
+ return _status(name, rest, detail=_named_root(argv))
607
+
608
+ rest, want_container = _pop_in_docker(rest)
609
+ if want_container and prof:
610
+ err(f"--in-docker doesn't apply to profile {name!r} — a profile "
611
+ f"carries its own mode, and this one is "
612
+ f"mode: {'docker' if prof.is_docker else 'host'}.")
613
+ say(f" {DIM}Just:{RESET} painapple {verb} {name}")
614
+ return 1
615
+
616
+ is_docker = (prof.is_docker if prof
617
+ else want_container or verb in DOCKER_ONLY)
618
+
619
+ if verb == "logs":
620
+ if is_docker:
621
+ from painapple_code.cli.deploy.container import cmd_logs
622
+ cmd_logs(_docker_settings_for(prof))
623
+ return 0
624
+ home = prof.home() if prof else serve_config.home_root()
625
+ _host_logs(home)
626
+ return 0
627
+
628
+ if verb == "password":
629
+ if is_docker:
630
+ from painapple_code.cli.deploy.container import cmd_password
631
+ return cmd_password(_docker_settings_for(prof), profile=name) or 0
632
+ # Root, and no host server has ever written a config: before
633
+ # dying with "has the server ever started?", check whether the
634
+ # ad-hoc sandbox is what they mean — it IS started, and its
635
+ # login page is what sent them here. Only on the empty path,
636
+ # so the common case never pays for a runtime probe.
637
+ if (prof is None and not _host_password_value()
638
+ and _adhoc_container_running()):
639
+ from painapple_code.cli.deploy.container import cmd_password
640
+ return cmd_password(_root_docker_settings()) or 0
641
+ vals = prof.host_values() if prof else serve_config.load()[0]
642
+ return _host_password(vals, name)
643
+
644
+ if verb in DOCKER_ONLY:
645
+ if prof and not prof.is_docker:
646
+ err(f"'{verb}' is a docker-mode verb, and profile {name!r} "
647
+ f"is mode: host.")
648
+ if verb == "shell":
649
+ say(f" {DIM}A host deployment is just this machine — "
650
+ f"open a terminal in its workspace.{RESET}")
651
+ return 1
652
+ from painapple_code.cli.deploy import container
653
+ cfg = _docker_settings_for(prof)
654
+ if verb == "shell":
655
+ container.cmd_shell(cfg, profile=name)
656
+ return 0
657
+ if verb == "claude-login":
658
+ container.cmd_claude_login(cfg, profile=name)
659
+ return 0
660
+ if verb == "extract":
661
+ return container.cmd_extract(cfg, rest, profile=name) or 0
662
+
663
+ err(f"Unknown verb: {verb}")
664
+ return 2
665
+ except ValueError as e:
666
+ err(str(e))
667
+ return 2
668
+ except KeyboardInterrupt:
669
+ say()
670
+ return 130