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,125 @@
1
+ """Host network introspection shared by the setup wizards and the
2
+ container launch path. Import-cheap: stdlib plus psutil, both imported
3
+ inside the functions so importing this module stays nearly free."""
4
+
5
+ import sys
6
+
7
+
8
+ def detect_local_ips():
9
+ """[(ip, interface)] for every non-loopback IPv4 the host has.
10
+
11
+ psutil rather than parsing `ip`/`ifconfig`: neither exists on Windows,
12
+ and ipconfig's output is localized (and OEM-encoded), so scraping it
13
+ would break on any non-English install. psutil is a hard dependency
14
+ (requirements.txt, no environment marker), so there is no fallback to
15
+ keep — the old scrape sat behind an ImportError that cannot fire.
16
+ """
17
+ import socket
18
+
19
+ import psutil
20
+
21
+ results = []
22
+ try:
23
+ stats = psutil.net_if_stats()
24
+ for iface, addrs in psutil.net_if_addrs().items():
25
+ st = stats.get(iface)
26
+ if st is not None and not st.isup:
27
+ continue
28
+ for addr in addrs:
29
+ if addr.family != socket.AF_INET:
30
+ continue
31
+ ip = addr.address
32
+ if ip.startswith("127.") or iface in ("lo", "lo0"):
33
+ continue
34
+ results.append((ip, iface))
35
+ except Exception:
36
+ pass # exotic/unsupported interface table — report what we got
37
+ return results
38
+
39
+
40
+ def port_taken(host, port):
41
+ """Reason string when `host:port` can't be bound, '' when it's free.
42
+
43
+ A plain bind test — catches ANY holder, unlike a scan of painapple's
44
+ own process table. Used by the server's own pre-flight and by
45
+ `painapple start` so the failure is reported before a detached child
46
+ inherits it. Unresolvable host counts as taken (with the reason)."""
47
+ import socket
48
+ try:
49
+ infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
50
+ except socket.gaierror as e:
51
+ return f"cannot resolve host {host!r} ({e})"
52
+ family, socktype, proto, _canon, sockaddr = infos[0]
53
+ with socket.socket(family, socktype, proto) as sock:
54
+ # SO_REUSEADDR means opposite things on the two platforms. On POSIX
55
+ # it only skips the TIME_WAIT guard, which is what we want here. On
56
+ # Windows it permits binding a port another socket is ACTIVELY
57
+ # listening on (Microsoft's documented port-hijacking behavior) —
58
+ # so this probe would report "free" while a server was running, and
59
+ # every caller (boot pre-flight, `painapple start`, the container
60
+ # launcher) would happily start a second instance on the same port,
61
+ # after which Windows routes new connections to whichever bound
62
+ # last. Setting nothing on win32 gives the strict bind we need.
63
+ if sys.platform != "win32":
64
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
65
+ try:
66
+ sock.bind(sockaddr)
67
+ except OSError as e:
68
+ return e.strerror or str(e)
69
+ return ""
70
+
71
+
72
+ def _listener_pid(port):
73
+ """PID of whatever holds the listening socket on `port`, or None.
74
+ Never raises — every caller is decoration."""
75
+ try:
76
+ import psutil
77
+ for conn in psutil.net_connections(kind="inet"):
78
+ if (conn.status == psutil.CONN_LISTEN and conn.laddr
79
+ and conn.laddr.port == int(port) and conn.pid):
80
+ return conn.pid
81
+ except Exception:
82
+ pass # AccessDenied for other users' sockets
83
+ return None
84
+
85
+
86
+ def port_holder(port):
87
+ """Best-effort description of what already listens on `port` — the
88
+ painapple instance if it's one of ours, else whatever ss/lsof names.
89
+ '' when nothing can be determined. Never raises (decoration only)."""
90
+ try:
91
+ from painapple_code.cli.list_cmd import local_servers
92
+ rows = [r for r in local_servers() if str(r["port"]) == str(port)]
93
+ # One server can present as SEVERAL rows on Windows: the pipx
94
+ # console-script launcher (painapple.exe), the python.exe it
95
+ # spawns, and uvicorn's reload child all carry the same argv, so
96
+ # all three match. Picking the first gave whichever the process
97
+ # scan happened to list first — usually the launcher shim, whose
98
+ # cwd is %TEMP%, so the error blamed a real port conflict on a
99
+ # workspace the user had never heard of. Prefer the row that
100
+ # actually owns the listening socket.
101
+ listener = _listener_pid(port)
102
+ row = next((r for r in rows if r["pid"] == listener), None)
103
+ if row is None and rows:
104
+ row = rows[0]
105
+ if row:
106
+ label = row.get("name") or "unnamed instance"
107
+ return (f"painapple '{label}' (pid {row['pid']}, "
108
+ f"workspace {row.get('workspace') or '?'})")
109
+ except Exception:
110
+ pass
111
+
112
+ # psutil: works on all three platforms and needs no ss/lsof. (On
113
+ # Windows neither exists, so the holder was always reported as unknown
114
+ # — the user got "port in use" with no way to tell what by.) The ss/
115
+ # lsof scrape that used to follow this was unreachable-by-design
116
+ # fallback: psutil is a hard dependency, so it only ever ran when
117
+ # psutil ALSO failed, in which case ss wouldn't have known better.
118
+ holder = _listener_pid(port)
119
+ if holder is not None:
120
+ try:
121
+ import psutil
122
+ return f"{psutil.Process(holder).name()} (pid {holder})"
123
+ except Exception:
124
+ return f"pid {holder}"
125
+ return ""
@@ -0,0 +1,390 @@
1
+ """Unified profile store — named deployments with a run mode.
2
+
3
+ A profile is a named, independent deployment at
4
+ ``~/.painapple-code/profiles/NAME/profile.yaml`` whose ``mode`` says
5
+ HOW it runs:
6
+
7
+ * ``mode: host`` — a local server. The profile dir IS its data home
8
+ (sessions, shadow DB, logs, config.json) — isolation is mandatory,
9
+ not cosmetic, since DuckDB is single-writer. Running it repoints
10
+ ``PAINAPPLE_CODE_HOME`` at the dir before the server boots
11
+ (:func:`activate`); every path helper follows for free.
12
+ * ``mode: docker`` — a container sandbox. Only ``profile.yaml`` lives
13
+ in the profile dir; data lives in the container's volume (``/data``).
14
+ ``claude_home`` defaults to the shared ``shared/.claude`` so one
15
+ login serves every sandbox.
16
+
17
+ Shared keys use the serve vocabulary (``host`` = bind, ``tls``,
18
+ ``port``, ``workspace``, ``instance_name``, ``accent``); docker mode
19
+ adds its own (image, container, volumes, workspace_mode, …) — see
20
+ cli/deploy/config.py for that half of the model.
21
+
22
+ Migration: :func:`ensure_migrated` adopts the two pre-unification
23
+ stores — ``serve-profiles/NAME/serve.yaml`` (moved wholesale; a
24
+ symlink is left behind so hand-written service units keep working) and
25
+ ``docker-profiles/NAME/docker.yaml`` — plus the root ``docker.yaml``
26
+ (runtime defaults merge into serve.yaml; the deployment itself is
27
+ adopted as a profile only if its container/volume actually exists).
28
+ Idempotent and loud.
29
+ """
30
+
31
+ import os
32
+ import re
33
+ import shutil
34
+ from pathlib import Path
35
+
36
+ from painapple_code.cli import serve_config
37
+ from painapple_code.cli.serve_config import (
38
+ HOST_KEYS, LEGACY_DOCKER_PROFILES_DIR, LEGACY_SERVE_PROFILES_DIR,
39
+ PROFILES_DIR, root_home,
40
+ )
41
+
42
+ PROFILE_FILE = "profile.yaml"
43
+ MODES = ("host", "docker")
44
+
45
+ _PROFILE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$")
46
+
47
+ # 'default' means the flag-less root deployment in every verb — a
48
+ # profile by that name could never be addressed.
49
+ _RESERVED_NAMES = {"default"}
50
+
51
+
52
+ def valid_name(name):
53
+ name = (name or "").strip()
54
+ return bool(_PROFILE_RE.match(name)) and name not in _RESERVED_NAMES
55
+
56
+
57
+ def profiles_root(root=None):
58
+ return (root or root_home()) / PROFILES_DIR
59
+
60
+
61
+ def profile_home(name, root=None):
62
+ return profiles_root(root) / name
63
+
64
+
65
+ def profile_path(name, root=None):
66
+ return profile_home(name, root) / PROFILE_FILE
67
+
68
+
69
+ def exists(name, root=None):
70
+ return profile_path(name, root).is_file()
71
+
72
+
73
+ def list_profiles(root=None):
74
+ """Named profiles that have a profile.yaml on disk (sorted)."""
75
+ base = profiles_root(root)
76
+ if not base.is_dir():
77
+ return []
78
+ return sorted(p.name for p in base.iterdir()
79
+ if p.is_dir() and (p / PROFILE_FILE).is_file())
80
+
81
+
82
+ class Profile:
83
+ """One loaded profile: ``name``, ``mode``, raw ``data`` dict, and
84
+ the load ``problems`` list (unknown/bad values, reported not fatal)."""
85
+
86
+ def __init__(self, name, mode, data, problems=None):
87
+ self.name = name
88
+ self.mode = mode
89
+ self.data = data
90
+ self.problems = problems or []
91
+
92
+ @property
93
+ def is_docker(self):
94
+ return self.mode == "docker"
95
+
96
+ def home(self, root=None):
97
+ return profile_home(self.name, root)
98
+
99
+ def host_values(self):
100
+ """Validated host-vocabulary subset (what the server layers /
101
+ what list & lifecycle read for a host profile)."""
102
+ values = {}
103
+ for key in HOST_KEYS:
104
+ raw = self.data.get(key)
105
+ if raw in (None, ""):
106
+ continue
107
+ try:
108
+ values[key] = serve_config._check(key, raw)
109
+ except ValueError:
110
+ continue
111
+ return values
112
+
113
+ def docker_settings(self):
114
+ """DockerSettings for a docker-mode profile (lazy import — keeps
115
+ this module light for the activation path)."""
116
+ from painapple_code.cli.deploy.config import settings_from_data
117
+ return settings_from_data(self.data, profile=self.name)
118
+
119
+ def label(self):
120
+ return self.data.get("instance_name") or self.name
121
+
122
+
123
+ def load(name, root=None):
124
+ """Load ``profiles/NAME/profile.yaml`` → :class:`Profile`. Returns
125
+ None when the profile doesn't exist. Never raises on bad content —
126
+ problems are collected on the returned object."""
127
+ path = profile_path(name, root)
128
+ if not path.is_file():
129
+ return None
130
+ import yaml
131
+ problems = []
132
+ try:
133
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
134
+ except (OSError, yaml.YAMLError) as e:
135
+ return Profile(name, "host", {}, [f"unreadable ({e})"])
136
+ if not isinstance(data, dict):
137
+ return Profile(name, "host", {}, ["not a key: value mapping"])
138
+ mode = str(data.pop("mode", "host") or "host").strip()
139
+ if mode not in MODES:
140
+ problems.append(f"unknown mode {mode!r} — treating as 'host'")
141
+ mode = "host"
142
+ return Profile(name, mode, data, problems)
143
+
144
+
145
+ def save(name, mode, data, root=None):
146
+ """Write ``profiles/NAME/profile.yaml``. ``data`` keys with empty
147
+ values are dropped. Returns the path written."""
148
+ if not valid_name(name):
149
+ raise ValueError(
150
+ f"Bad profile name: {name!r} (letters/digits/._- only, max 32 chars)")
151
+ if mode not in MODES:
152
+ raise ValueError(f"Bad profile mode: {mode!r}")
153
+ import yaml
154
+ path = profile_path(name, root)
155
+ path.parent.mkdir(parents=True, exist_ok=True)
156
+ kept = {"mode": mode}
157
+ for key, value in data.items():
158
+ if value in (None, "", []):
159
+ continue
160
+ kept[key] = value
161
+ body = yaml.safe_dump(kept, default_flow_style=False, sort_keys=False)
162
+ path.write_text(
163
+ f"# Profile '{name}' — written by `painapple setup {name}`, editable by hand.\n"
164
+ "# mode: host (local server; this dir is its data home) or docker (container).\n"
165
+ + body, encoding="utf-8")
166
+ from painapple_code.paths import lock_mode
167
+ lock_mode(path, 0o600) # icacls on win32; chmod is a no-op there
168
+ return path
169
+
170
+
171
+ def resolve_home(name, root):
172
+ """The data home ``--profile NAME`` resolves to, given the launching
173
+ process's own PAINAPPLE_CODE_HOME (``root``). Pure path arithmetic,
174
+ creating nothing — :func:`activate` uses it for this process, and the
175
+ fleet-view scan uses it to reproduce the same answer for SOMEONE
176
+ ELSE's process from its argv. Unusable names fall back to ``root``.
177
+ """
178
+ name = (name or "").strip()
179
+ root = Path(root)
180
+ if name in ("", "default") or not valid_name(name):
181
+ return root
182
+ if root.name == name and root.parent.name in (PROFILES_DIR,
183
+ LEGACY_SERVE_PROFILES_DIR):
184
+ # PAINAPPLE_CODE_HOME already points AT this profile's home (a
185
+ # spawner — `painapple start`, a service unit — exported both
186
+ # env vars). Re-appending profiles/NAME would nest a second home
187
+ # inside the first; adopt the given home instead.
188
+ return root
189
+ return root / PROFILES_DIR / name
190
+
191
+
192
+ def activate(name):
193
+ """Repoint PAINAPPLE_CODE_HOME at a HOST profile's data home
194
+ (created if absent) and record the name in PAINAPPLE_PROFILE, so
195
+ every downstream path helper — and the config layering — targets
196
+ the profile. ``''``/``default`` is a no-op (the root deployment).
197
+ Returns the home Path. Raises ValueError on a bad name."""
198
+ name = (name or "").strip()
199
+ if name in ("", "default"):
200
+ return serve_config.home_root()
201
+ if not valid_name(name):
202
+ raise ValueError(
203
+ f"Bad profile name: {name!r} (letters/digits/._- only, max 32 chars)")
204
+ home = resolve_home(name, serve_config.home_root())
205
+ home.mkdir(parents=True, exist_ok=True)
206
+ os.environ["PAINAPPLE_CODE_HOME"] = str(home)
207
+ os.environ["PAINAPPLE_PROFILE"] = name
208
+ return home
209
+
210
+
211
+ # ──── Migration (serve-profiles/ + docker-profiles/ + root docker.yaml) ──
212
+
213
+ _migrated_this_process = False
214
+
215
+
216
+ def _free_name(name, taken):
217
+ if name not in taken and not exists(name):
218
+ return name
219
+ for candidate in (f"{name}-docker", *(f"{name}-{i}" for i in range(2, 20))):
220
+ if candidate not in taken and not exists(candidate):
221
+ return candidate
222
+ return None
223
+
224
+
225
+ def _adopt_serve_profile(root, name, notes):
226
+ src = root / LEGACY_SERVE_PROFILES_DIR / name
227
+ if exists(name, root):
228
+ notes.append(f"'{name}': profiles/{name} already exists — "
229
+ f"left {src} untouched")
230
+ return
231
+ dst = profile_home(name, root)
232
+ dst.parent.mkdir(parents=True, exist_ok=True)
233
+ import yaml
234
+ try:
235
+ data = yaml.safe_load((src / "serve.yaml").read_text(encoding="utf-8")) or {}
236
+ except (OSError, yaml.YAMLError) as e:
237
+ notes.append(f"'{name}': unreadable serve.yaml ({e}) — skipped")
238
+ return
239
+ if not isinstance(data, dict):
240
+ data = {}
241
+ # Move the WHOLE dir — it's the profile's data home (sessions,
242
+ # DuckDB, logs ride along).
243
+ shutil.move(str(src), str(dst))
244
+ (dst / "serve.yaml").unlink(missing_ok=True)
245
+ save(name, "host", data, root)
246
+ # Leave a symlink so hand-written service units / scripts that
247
+ # exported PAINAPPLE_CODE_HOME=…/serve-profiles/NAME keep working.
248
+ try:
249
+ src.symlink_to(Path("..") / PROFILES_DIR / name)
250
+ except OSError:
251
+ pass
252
+ notes.append(f"adopted serve profile '{name}' → profiles/{name} (mode: host)")
253
+
254
+
255
+ def _adopt_docker_profile(root, name, notes):
256
+ src = root / LEGACY_DOCKER_PROFILES_DIR / name
257
+ legacy_yaml = src / "docker.yaml"
258
+ if not legacy_yaml.is_file():
259
+ notes.append(f"'{name}': wrapper.conf-only docker profile — not "
260
+ f"adopted (re-create with: painapple setup {name})")
261
+ return
262
+ target = _free_name(name, set())
263
+ if target is None:
264
+ notes.append(f"'{name}': no free profile name — skipped")
265
+ return
266
+ import yaml
267
+ try:
268
+ data = yaml.safe_load(legacy_yaml.read_text(encoding="utf-8")) or {}
269
+ except (OSError, yaml.YAMLError) as e:
270
+ notes.append(f"'{name}': unreadable docker.yaml ({e}) — skipped")
271
+ return
272
+ from painapple_code.cli.deploy.config import (
273
+ settings_from_data, settings_to_data)
274
+ settings = settings_from_data(data, profile=name)
275
+ save(target, "docker", settings_to_data(settings), root)
276
+ for leftover in ("docker.yaml", "wrapper.conf"):
277
+ (src / leftover).unlink(missing_ok=True)
278
+ try:
279
+ src.rmdir()
280
+ except OSError:
281
+ pass # stray files — leave the dir
282
+ suffix = "" if target == name else f" (renamed — '{name}' was taken)"
283
+ notes.append(f"adopted docker profile '{name}' → profiles/{target} "
284
+ f"(mode: docker){suffix}")
285
+
286
+
287
+ def _adopt_root_docker(root, notes):
288
+ legacy_yaml = root / "docker.yaml"
289
+ if not legacy_yaml.is_file():
290
+ return
291
+ import yaml
292
+ try:
293
+ data = yaml.safe_load(legacy_yaml.read_text(encoding="utf-8")) or {}
294
+ except (OSError, yaml.YAMLError) as e:
295
+ notes.append(f"root docker.yaml unreadable ({e}) — left in place")
296
+ return
297
+ if not isinstance(data, dict):
298
+ data = {}
299
+
300
+ # 1. Runtime/image defaults → the global serve.yaml (the
301
+ # `--in-docker` defaults), only where serve.yaml doesn't already
302
+ # have a value.
303
+ current, _ = serve_config.load()
304
+ merged = dict(current)
305
+ took = []
306
+ for key in serve_config.DEPLOY_KEYS:
307
+ if merged.get(key) in (None, "") and data.get(key) not in (None, ""):
308
+ merged[key] = str(data[key])
309
+ took.append(key)
310
+ if took:
311
+ try:
312
+ serve_config.save(merged)
313
+ notes.append("root docker.yaml: merged "
314
+ f"{', '.join(took)} into serve.yaml (--in-docker defaults)")
315
+ except ValueError as e:
316
+ notes.append(f"root docker.yaml: could not merge defaults ({e})")
317
+
318
+ # 2. The deployment itself becomes a profile only if it was real —
319
+ # a container or volume with its configured names exists.
320
+ from painapple_code.cli.deploy.config import (
321
+ settings_from_data, settings_to_data)
322
+ settings = settings_from_data(data, profile=None)
323
+ live = False
324
+ if shutil.which("docker") or shutil.which("podman") or settings.runtime:
325
+ try:
326
+ from painapple_code.cli.deploy.runtime import Runtime
327
+ rt = Runtime(settings)
328
+ live = (rt.container_exists(settings.container)
329
+ or (not settings.data_is_bind()
330
+ and rt.volume_exists(settings.data_volume)))
331
+ except (SystemExit, OSError):
332
+ live = False
333
+ if live:
334
+ target = _free_name("docker", set())
335
+ if target:
336
+ save(target, "docker", settings_to_data(settings), root)
337
+ notes.append(f"adopted the root docker deployment → "
338
+ f"profiles/{target} (mode: docker)")
339
+
340
+ # os.replace: a re-run after a partial migration already left a
341
+ # .migrated file behind — Path.rename would raise on win32 then.
342
+ os.replace(legacy_yaml, legacy_yaml.with_suffix(".yaml.migrated"))
343
+ notes.append("root docker.yaml → docker.yaml.migrated (superseded by "
344
+ "serve.yaml + profiles)")
345
+
346
+
347
+ def ensure_migrated(announce=None):
348
+ """Adopt pre-unification stores into ``profiles/``. Idempotent;
349
+ cheap when there's nothing to do. ``announce`` is called with one
350
+ human line per action (default: cli.ui.info)."""
351
+ global _migrated_this_process
352
+ if _migrated_this_process:
353
+ return []
354
+ _migrated_this_process = True
355
+
356
+ root = root_home()
357
+ serve_dir = root / LEGACY_SERVE_PROFILES_DIR
358
+ docker_dir = root / LEGACY_DOCKER_PROFILES_DIR
359
+ serve_names = []
360
+ if serve_dir.is_dir():
361
+ serve_names = sorted(
362
+ p.name for p in serve_dir.iterdir()
363
+ if p.is_dir() and not p.is_symlink() and (p / "serve.yaml").is_file())
364
+ docker_names = []
365
+ if docker_dir.is_dir():
366
+ docker_names = sorted(
367
+ p.name for p in docker_dir.iterdir()
368
+ if p.is_dir() and ((p / "docker.yaml").is_file()
369
+ or (p / "wrapper.conf").is_file()))
370
+ has_root_docker = (root / "docker.yaml").is_file()
371
+ if not serve_names and not docker_names and not has_root_docker:
372
+ return []
373
+
374
+ notes = []
375
+ for name in serve_names: # host profiles first: name wins
376
+ _adopt_serve_profile(root, name, notes)
377
+ for name in docker_names:
378
+ _adopt_docker_profile(root, name, notes)
379
+ if has_root_docker:
380
+ _adopt_root_docker(root, notes)
381
+
382
+ if notes:
383
+ if announce is None:
384
+ from painapple_code.cli.ui import info
385
+ announce = info
386
+ announce("Migrating to the unified profile store "
387
+ f"({profiles_root(root)}):")
388
+ for note in notes:
389
+ announce(f" {note}")
390
+ return notes
@@ -0,0 +1,163 @@
1
+ """Argument parser for ``painapple serve`` — the single source of truth.
2
+
3
+ IMPORT-LIGHT ZONE: this module is the fast path for ``-v`` /
4
+ ``serve --help`` / argparse errors, which ``cli.main()`` resolves
5
+ *before* paying the ~300ms ``painapple_code.server`` import. It may
6
+ import ``argparse``, ``os``, ``sys``, and ``painapple_code.__version__``
7
+ — NOTHING else. No server, no fastapi, no routes, no yaml.
8
+ ``tests/test_cli_startup.py`` asserts fastapi stays unimported.
9
+
10
+ Both callers build from here, so help/error output and parsing are
11
+ byte-identical by construction:
12
+
13
+ - ``cli.main()`` — the fast gate (parse, then discard)
14
+ - ``server.main()`` — the real boot (re-parse, ~1ms, keeps its
15
+ signature and late ``parser.error()`` calls unchanged)
16
+ """
17
+
18
+ import argparse
19
+ import os
20
+ import sys
21
+
22
+ from painapple_code import __version__
23
+
24
+ #: Default bind port. Shared with ``server.resolve_allowed_origins()``, which
25
+ #: derives its no-config trusted-origin fallback from it — keep it a constant so
26
+ #: the two can't drift (they did: a hardcoded origin list outlived a port
27
+ #: renumber and shipped a stale port as a trusted default for months).
28
+ DEFAULT_PORT = 8765
29
+
30
+
31
+ def _subcommands_epilog():
32
+ """Epilog for `serve --help` pointing back at the command overview
33
+ and the other subcommands (the curated front door is `painapple
34
+ help`; this page is deliberately just the full serve-flag dump).
35
+
36
+ argparse colorizes its own generated help (usage, option strings,
37
+ section headings) on Python 3.14+, but passes this free-form epilog
38
+ through verbatim — so it renders plain while everything above it is
39
+ colored. Match argparse's own decision: colorize the block only when
40
+ argparse would (3.14+, a TTY, and NO_COLOR unset), otherwise emit
41
+ plain text. ANSI codes have zero display width, so column alignment
42
+ is unaffected.
43
+ """
44
+ color = (
45
+ sys.version_info >= (3, 14)
46
+ and sys.stdout.isatty()
47
+ and not os.environ.get("NO_COLOR")
48
+ )
49
+ b = "\033[1m" if color else "" # bold heading
50
+ g = "\033[32m" if color else "" # green subcommand
51
+ d = "\033[2m" if color else "" # dim description
52
+ r = "\033[0m" if color else ""
53
+ return "\n".join((
54
+ f"{b}profiles{r}:",
55
+ f" painapple {g}--profile NAME{r} {d}run a named deployment in the foreground (own port/data/sessions){r}",
56
+ f" painapple {g}setup NAME{r} {d}create/edit that profile (host or docker mode){r}",
57
+ "",
58
+ f"{b}more{r}:",
59
+ f" painapple {g}help{r} {d}command overview (setup · list · start/stop/restart · status · …){r}",
60
+ f" painapple {g}setup{r} {d}save global defaults for these flags (~/.painapple-code/serve.yaml){r}",
61
+ f" painapple {g}list{r} {d}every instance on this machine (profiles + running processes){r}",
62
+ ))
63
+
64
+
65
+ def build_parser():
66
+ """The full ``painapple serve`` flag parser."""
67
+ parser = argparse.ArgumentParser(
68
+ prog="painapple", description="pAInapple Code Server",
69
+ usage="painapple [serve] [--port N] [--workspace PATH] [flags…]",
70
+ formatter_class=argparse.RawDescriptionHelpFormatter,
71
+ epilog=_subcommands_epilog())
72
+ parser.add_argument("-v", "--version", action="version",
73
+ version=f"painapple {__version__}")
74
+
75
+ core = parser.add_argument_group(
76
+ "server", "where it listens and what it works on")
77
+ core.add_argument("--host", default="127.0.0.1",
78
+ help="Host interface to bind (default 127.0.0.1 — this machine "
79
+ "only; 0.0.0.0 = every interface, reachable on your LAN)")
80
+ core.add_argument("--port", type=int, default=DEFAULT_PORT, help="Port to bind to")
81
+ core.add_argument("--workspace", "--cwd", dest="workspace", default=".",
82
+ help="Workspace root — the directory holding your projects. "
83
+ "You pick the actual project in-app from the welcome "
84
+ "screen, and each session's files/git/terminal follow "
85
+ "its own directory. This anchors the file explorer and "
86
+ "the welcome screen's project suggestions. Mapped to "
87
+ "/workspace inside Docker. (--cwd is an alias)")
88
+ core.add_argument("--instance-name", default=None,
89
+ help="Instance label for PWA icon and UI (e.g., DEV, STABLE)")
90
+ core.add_argument("--accent", default=None,
91
+ help="Accent color: preset (blue/green/red/orange/purple/cyan/gray/yellow/pink/teal/indigo/lime) or hex (#f87171)")
92
+ core.add_argument("--in-docker", action="store_true",
93
+ help="Run this same invocation inside a container instead "
94
+ "(Docker/Podman): the workspace (default: current dir) is "
95
+ "mounted into the prebuilt image, foreground, Ctrl-C stops "
96
+ "it. Runtime/image defaults come from `painapple setup`; "
97
+ "fetch the image once with `painapple pull`.")
98
+
99
+ net = parser.add_argument_group(
100
+ "network security", "TLS for non-loopback binds")
101
+ net.add_argument("--tls", choices=("auto", "on", "off"), default="auto",
102
+ help="TLS mode. 'auto' (default) enables TLS when binding to a non-loopback "
103
+ "host. 'on' forces TLS. 'off' disables it. Self-signed cert is "
104
+ "auto-generated; clients accept it without verification (TLS here "
105
+ "guards against passive snooping only).")
106
+ pw = net.add_mutually_exclusive_group()
107
+ pw.add_argument("--no-password", "--no-passwd", dest="no_password",
108
+ action="store_true",
109
+ help="Never print credentials to stdout: the startup box hides "
110
+ "the password and strips the ?tkn= from the login URL. "
111
+ "Retrieve them later with `painapple password` or from the "
112
+ "auth config file. Useful when the console is visible to "
113
+ "others (screen shares, recorded demos, shared logs). "
114
+ "This is already the default on non-loopback binds.")
115
+ pw.add_argument("--show-password", dest="show_password",
116
+ action="store_true",
117
+ help="Print credentials even on a non-loopback bind. By default "
118
+ "only loopback binds (127.0.0.1/::1/localhost) show the "
119
+ "password and ?tkn= login URL — a LAN/public server's "
120
+ "stdout tends to outlive the terminal (journald, docker "
121
+ "logs), so there they're hidden unless you opt in.")
122
+ net.add_argument("--tls-cert", default=None, metavar="PATH",
123
+ help="TLS cert path (default: <config-dir>/cert.pem, auto-generated)")
124
+ net.add_argument("--tls-key", default=None, metavar="PATH",
125
+ help="TLS key path (default: <config-dir>/key.pem, auto-generated)")
126
+
127
+ adv = parser.add_argument_group(
128
+ "advanced", "provider choice and opt-in extras")
129
+ adv.add_argument("--default-provider", default=None, metavar="NAME",
130
+ help="Provider for NEW sessions (existing sessions keep their "
131
+ "recorded provider). Overrides the `default_provider` "
132
+ "global-config key. E.g. 'claude-sdk' (Agent SDK driver — "
133
+ "interactive permission cards, live mode/model switching; "
134
+ "default) or 'claude' (line protocol).")
135
+ adv.add_argument("--public-origin", action="append", default=None, metavar="ORIGIN",
136
+ help="Trusted browser origin for a proxied deployment, e.g. "
137
+ "https://claude.example.com. Repeatable. Adds to the "
138
+ "HTTP/WebSocket Origin allowlist (and replaces the default, "
139
+ "which trusts only loopback on the bound port). Also "
140
+ "honoured via PAINAPPLE_ALLOWED_ORIGINS.")
141
+ adv.add_argument("--enable-renderers", action="store_true",
142
+ help="Enable server-side Vega-Lite/Excalidraw rendering (off by "
143
+ "default: model-authored specs are rendered via a Node "
144
+ "subprocess whose data loader can fetch external/file URLs — "
145
+ "an SSRF/local-file-read vector. Also honoured via the "
146
+ "PAINAPPLE_ENABLE_RENDERERS env var).")
147
+
148
+ tiers = parser.add_argument_group(
149
+ "multi-instance", "isolate state when several servers share one user "
150
+ "(each instance needs its own shadow DB — DuckDB is single-writer)")
151
+ tiers.add_argument("--shadow-db", default=None, metavar="PATH",
152
+ help="DuckDB path for shadow DB (default: ~/.painapple-code/shadow.duckdb)")
153
+ tiers.add_argument("--log-dir", default=None, metavar="PATH",
154
+ help="Log directory (default: ~/.painapple-code/logs/)")
155
+ tiers.add_argument("--state-suffix", default=None, metavar="SUFFIX",
156
+ help="Per-tier suffix for UI-state files (tab-state, shortcuts, "
157
+ "presets, favorites, global config) so co-located tiers don't "
158
+ "share them, e.g. 'dev' gives tab-state-dev.json (a leading "
159
+ "'-' is added automatically). Project and session history "
160
+ "stays shared. Default: none (stable tier).")
161
+ tiers.add_argument("--auth-config-file", default=None, metavar="PATH",
162
+ help="Config file path (default: ~/.config/painapple-code/config.yaml)")
163
+ return parser