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.
- painapple_code/__init__.py +30 -0
- painapple_code/__main__.py +18 -0
- painapple_code/_version.py +24 -0
- painapple_code/auth_middleware.py +930 -0
- painapple_code/cli/__init__.py +204 -0
- painapple_code/cli/deploy/__init__.py +15 -0
- painapple_code/cli/deploy/claude_seed.py +41 -0
- painapple_code/cli/deploy/config.py +260 -0
- painapple_code/cli/deploy/container.py +715 -0
- painapple_code/cli/deploy/launch.py +163 -0
- painapple_code/cli/deploy/runtime.py +223 -0
- painapple_code/cli/lifecycle_cmd.py +443 -0
- painapple_code/cli/list_cmd.py +572 -0
- painapple_code/cli/manage_cmd.py +670 -0
- painapple_code/cli/netinfo.py +125 -0
- painapple_code/cli/profiles.py +390 -0
- painapple_code/cli/serve_args.py +163 -0
- painapple_code/cli/serve_config.py +256 -0
- painapple_code/cli/setup_cmd.py +664 -0
- painapple_code/cli/ui.py +438 -0
- painapple_code/cost_analytics.py +879 -0
- painapple_code/data/models.defaults.yaml +18 -0
- painapple_code/data/models.yaml +18 -0
- painapple_code/data/presets.defaults.json +32 -0
- painapple_code/data/strings.yaml +2269 -0
- painapple_code/helpers.py +406 -0
- painapple_code/paths.py +1514 -0
- painapple_code/prompt_explorer.py +984 -0
- painapple_code/providers/__init__.py +224 -0
- painapple_code/providers/base.py +710 -0
- painapple_code/providers/claude/__init__.py +74 -0
- painapple_code/providers/claude/capabilities.py +218 -0
- painapple_code/providers/claude/errors.py +79 -0
- painapple_code/providers/claude/launch.py +50 -0
- painapple_code/providers/claude/summary.py +89 -0
- painapple_code/providers/claude/translate.py +74 -0
- painapple_code/providers/claude_sdk/__init__.py +139 -0
- painapple_code/providers/claude_sdk/driver.py +412 -0
- painapple_code/providers/codex/__init__.py +87 -0
- painapple_code/providers/codex/capabilities.py +321 -0
- painapple_code/providers/codex/errors.py +70 -0
- painapple_code/providers/codex/launch.py +142 -0
- painapple_code/providers/codex/summary.py +196 -0
- painapple_code/providers/codex/translate.py +204 -0
- painapple_code/providers/codex_app_server/__init__.py +100 -0
- painapple_code/providers/codex_app_server/capabilities.py +41 -0
- painapple_code/providers/codex_app_server/launch.py +202 -0
- painapple_code/providers/codex_app_server/summary.py +291 -0
- painapple_code/providers/codex_app_server/translate.py +263 -0
- painapple_code/providers/codex_app_server/transport.py +238 -0
- painapple_code/routes/__init__.py +0 -0
- painapple_code/routes/api_agents.py +441 -0
- painapple_code/routes/api_app.py +368 -0
- painapple_code/routes/api_app_commit_sections.py +127 -0
- painapple_code/routes/api_app_config.py +997 -0
- painapple_code/routes/api_app_session_prefs.py +217 -0
- painapple_code/routes/api_browser.py +690 -0
- painapple_code/routes/api_commands.py +374 -0
- painapple_code/routes/api_costs.py +128 -0
- painapple_code/routes/api_drafts.py +139 -0
- painapple_code/routes/api_exec.py +149 -0
- painapple_code/routes/api_files.py +542 -0
- painapple_code/routes/api_git.py +657 -0
- painapple_code/routes/api_logs.py +649 -0
- painapple_code/routes/api_plugins.py +275 -0
- painapple_code/routes/api_project_config.py +418 -0
- painapple_code/routes/api_prompts.py +231 -0
- painapple_code/routes/api_providers.py +53 -0
- painapple_code/routes/api_search.py +411 -0
- painapple_code/routes/api_session_stash.py +234 -0
- painapple_code/routes/api_session_welcome.py +151 -0
- painapple_code/routes/api_sessions.py +742 -0
- painapple_code/routes/api_shadow.py +323 -0
- painapple_code/routes/api_shadow_db.py +512 -0
- painapple_code/routes/api_shadow_files.py +332 -0
- painapple_code/routes/api_shadow_search.py +476 -0
- painapple_code/routes/api_skills.py +610 -0
- painapple_code/routes/api_tasks.py +147 -0
- painapple_code/routes/api_terminal.py +540 -0
- painapple_code/routes/api_upload.py +293 -0
- painapple_code/routes/api_viewer.py +229 -0
- painapple_code/routes/dependencies.py +109 -0
- painapple_code/routes/ws_chat.py +837 -0
- painapple_code/server.py +2208 -0
- painapple_code/server_logging.py +263 -0
- painapple_code/services/__init__.py +0 -0
- painapple_code/services/agent_session.py +2738 -0
- painapple_code/session_store.py +253 -0
- painapple_code/session_store_core.py +879 -0
- painapple_code/shadow_db.py +830 -0
- painapple_code/shadow_db_plans.py +369 -0
- painapple_code/shadow_db_queries.py +595 -0
- painapple_code/shadow_db_schema.py +676 -0
- painapple_code/shadow_git.py +1296 -0
- painapple_code/shadow_git_frontmatter.py +314 -0
- painapple_code/shadow_git_sections.py +520 -0
- painapple_code/shadow_git_summary.py +462 -0
- painapple_code/shadow_parser.py +560 -0
- painapple_code/static/css/00-variables.css +276 -0
- painapple_code/static/css/01-base.css +245 -0
- painapple_code/static/css/02-tooltips.css +36 -0
- painapple_code/static/css/09-left-rail.css +448 -0
- painapple_code/static/css/10-header.css +777 -0
- painapple_code/static/css/11-connection-bar.css +120 -0
- painapple_code/static/css/20-messages.css +750 -0
- painapple_code/static/css/21-tool-blocks.css +2340 -0
- painapple_code/static/css/22-edit-diff.css +619 -0
- painapple_code/static/css/23-thinking.css +1584 -0
- painapple_code/static/css/24-background-tasks.css +359 -0
- painapple_code/static/css/25-context-block.css +252 -0
- painapple_code/static/css/26-session-setup.css +144 -0
- painapple_code/static/css/30-markdown.css +854 -0
- painapple_code/static/css/40-input.css +2418 -0
- painapple_code/static/css/41-autocomplete.css +127 -0
- painapple_code/static/css/42-activity-strip.css +97 -0
- painapple_code/static/css/43-file-autocomplete.css +121 -0
- painapple_code/static/css/44-snippets-autocomplete.css +224 -0
- painapple_code/static/css/45-selection-bar.css +433 -0
- painapple_code/static/css/46-keyboard-bar.css +119 -0
- painapple_code/static/css/47-keyboard-longpress.css +116 -0
- painapple_code/static/css/47-token-profile.css +106 -0
- painapple_code/static/css/50-welcome.css +3095 -0
- painapple_code/static/css/51-modals.css +301 -0
- painapple_code/static/css/52-utilities.css +77 -0
- painapple_code/static/css/53-image-upload.css +456 -0
- painapple_code/static/css/54-todo-list.css +102 -0
- painapple_code/static/css/55-file-upload.css +113 -0
- painapple_code/static/css/56-chat-navigator.css +218 -0
- painapple_code/static/css/56-permission-card.css +332 -0
- painapple_code/static/css/56-plan-approval.css +204 -0
- painapple_code/static/css/56-question-form.css +602 -0
- painapple_code/static/css/57-swipe-indicator.css +95 -0
- painapple_code/static/css/58-session-families.css +627 -0
- painapple_code/static/css/59-about.css +196 -0
- painapple_code/static/css/61-log-explorer.css +723 -0
- painapple_code/static/css/62-json-tree.css +489 -0
- painapple_code/static/css/62-skills-widget.css +650 -0
- painapple_code/static/css/63-commands-widget.css +30 -0
- painapple_code/static/css/63-plugins-widget.css +174 -0
- painapple_code/static/css/63-terminal.css +516 -0
- painapple_code/static/css/64-search-files-widget.css +448 -0
- painapple_code/static/css/65-snippets-widget.css +426 -0
- painapple_code/static/css/66-active-sessions.css +979 -0
- painapple_code/static/css/66-agents-widget.css +265 -0
- painapple_code/static/css/66-file-explorer-widget.css +855 -0
- painapple_code/static/css/66-git-panel.css +791 -0
- painapple_code/static/css/66-history-explorer.css +1496 -0
- painapple_code/static/css/67-diff-viewer.css +602 -0
- painapple_code/static/css/67-editor-view.css +748 -0
- painapple_code/static/css/67-file-preview-widget.css +1873 -0
- painapple_code/static/css/68-browser-widget.css +337 -0
- painapple_code/static/css/68-compare-wizard.css +355 -0
- painapple_code/static/css/68-csv-preview.css +106 -0
- painapple_code/static/css/68-diff-preview.css +103 -0
- painapple_code/static/css/68-helpers-install-widget.css +491 -0
- painapple_code/static/css/68-image-annotate.css +346 -0
- painapple_code/static/css/68-terminal-view.css +98 -0
- painapple_code/static/css/68-uploads-widget.css +229 -0
- painapple_code/static/css/69-cost-analytics.css +537 -0
- painapple_code/static/css/70-config-panel.css +3736 -0
- painapple_code/static/css/70-excalidraw.css +136 -0
- painapple_code/static/css/70-prompt-explorer.css +890 -0
- painapple_code/static/css/71-chart.css +111 -0
- painapple_code/static/css/72-discussion-sidebar.css +678 -0
- painapple_code/static/css/74-debug-logs.css +656 -0
- painapple_code/static/css/80-open-dialog.css +146 -0
- painapple_code/static/css/80-quick-switcher.css +383 -0
- painapple_code/static/css/80-widget-system.css +1763 -0
- painapple_code/static/css/81-quick-actions.css +1020 -0
- painapple_code/static/css/82-context-popover.css +1233 -0
- painapple_code/static/css/83-context-menu.css +339 -0
- painapple_code/static/css/85-lazy-loading.css +102 -0
- painapple_code/static/css/86-zen-mode.css +708 -0
- painapple_code/static/css/87-grid-switcher.css +346 -0
- painapple_code/static/css/login.css +198 -0
- painapple_code/static/dist/app.bundle.js +7177 -0
- painapple_code/static/feature-triage.html +446 -0
- painapple_code/static/icons/apple-touch-icon.png +0 -0
- painapple_code/static/icons/favicon-32.png +0 -0
- painapple_code/static/icons/favicon.ico +0 -0
- painapple_code/static/icons/icon-128.png +0 -0
- painapple_code/static/icons/icon-144.png +0 -0
- painapple_code/static/icons/icon-152.png +0 -0
- painapple_code/static/icons/icon-180.png +0 -0
- painapple_code/static/icons/icon-192.png +0 -0
- painapple_code/static/icons/icon-384.png +0 -0
- painapple_code/static/icons/icon-512.png +0 -0
- painapple_code/static/icons/icon-72.png +0 -0
- painapple_code/static/icons/icon-96.png +0 -0
- painapple_code/static/js/action-delegate.js +187 -0
- painapple_code/static/js/activity-strip.js +193 -0
- painapple_code/static/js/app/context-menus.js +619 -0
- painapple_code/static/js/app/file-preview.js +163 -0
- painapple_code/static/js/app/init-ui.js +574 -0
- painapple_code/static/js/app/input-events.js +561 -0
- painapple_code/static/js/app/input-resize.js +313 -0
- painapple_code/static/js/app/message-actions.js +419 -0
- painapple_code/static/js/app/panels.js +210 -0
- painapple_code/static/js/app/rendering-delegators.js +246 -0
- painapple_code/static/js/app/server-loading.js +365 -0
- painapple_code/static/js/app/session-ops.js +287 -0
- painapple_code/static/js/app/session-switch.js +443 -0
- painapple_code/static/js/app/settings-helpers.js +280 -0
- painapple_code/static/js/app/startup-welcome.js +290 -0
- painapple_code/static/js/app-context.js +157 -0
- painapple_code/static/js/app.js +2134 -0
- painapple_code/static/js/auth-fetch.js +46 -0
- painapple_code/static/js/background-tasks.js +274 -0
- painapple_code/static/js/boot.js +73 -0
- painapple_code/static/js/caret-position.js +171 -0
- painapple_code/static/js/chat-navigator.js +710 -0
- painapple_code/static/js/chat-search.js +323 -0
- painapple_code/static/js/command-executor.js +176 -0
- painapple_code/static/js/command-store.js +497 -0
- painapple_code/static/js/components.js +1414 -0
- painapple_code/static/js/config.js +218 -0
- painapple_code/static/js/context-menu.js +740 -0
- painapple_code/static/js/controllers/chat-controller.js +4252 -0
- painapple_code/static/js/controllers/dialog-controller.js +217 -0
- painapple_code/static/js/controllers/tab-controller.js +1828 -0
- painapple_code/static/js/controllers/thinking-controller.js +915 -0
- painapple_code/static/js/diff-utils.js +486 -0
- painapple_code/static/js/drafts-pill.js +95 -0
- painapple_code/static/js/editor-view.js +815 -0
- painapple_code/static/js/effort-settings.js +430 -0
- painapple_code/static/js/feature-triage.js +811 -0
- painapple_code/static/js/file-autocomplete.js +516 -0
- painapple_code/static/js/file-tabs.js +123 -0
- painapple_code/static/js/gestures.js +448 -0
- painapple_code/static/js/grid-switcher.js +334 -0
- painapple_code/static/js/input-handler.js +1472 -0
- painapple_code/static/js/keyboard-bar.js +495 -0
- painapple_code/static/js/layout-switcher.js +109 -0
- painapple_code/static/js/linkify-utils.js +200 -0
- painapple_code/static/js/login.js +141 -0
- painapple_code/static/js/open-dialog.js +879 -0
- painapple_code/static/js/orphan-terminals.js +373 -0
- painapple_code/static/js/path-utils.js +179 -0
- painapple_code/static/js/perf-marks.js +102 -0
- painapple_code/static/js/permission-settings.js +475 -0
- painapple_code/static/js/pointer-input.js +59 -0
- painapple_code/static/js/preview/json-tree-menu.js +226 -0
- painapple_code/static/js/preview/json-tree-toolbar.js +291 -0
- painapple_code/static/js/preview/json-tree.js +457 -0
- painapple_code/static/js/preview/preview-breadcrumb.js +387 -0
- painapple_code/static/js/preview/preview-edit.js +367 -0
- painapple_code/static/js/preview/preview-events.js +361 -0
- painapple_code/static/js/preview/preview-history.js +567 -0
- painapple_code/static/js/preview/preview-inline-edit.js +869 -0
- painapple_code/static/js/preview/preview-poll.js +108 -0
- painapple_code/static/js/preview/preview-render.js +572 -0
- painapple_code/static/js/preview/preview-search.js +421 -0
- painapple_code/static/js/preview/preview-state.js +256 -0
- painapple_code/static/js/preview/preview-utils.js +251 -0
- painapple_code/static/js/preview-plugins/chart-plugin.js +22 -0
- painapple_code/static/js/preview-plugins/csv-plugin.js +188 -0
- painapple_code/static/js/preview-plugins/diff-plugin.js +258 -0
- painapple_code/static/js/preview-plugins/excalidraw-plugin.js +25 -0
- painapple_code/static/js/preview-plugins/html-plugin.js +104 -0
- painapple_code/static/js/preview-plugins/image-plugin.js +20 -0
- painapple_code/static/js/preview-plugins/index.js +34 -0
- painapple_code/static/js/preview-plugins/json-plugin.js +99 -0
- painapple_code/static/js/preview-plugins/jsonl-plugin.js +152 -0
- painapple_code/static/js/preview-plugins/markdown-plugin.js +266 -0
- painapple_code/static/js/preview-plugins/panzoom-plugin.js +63 -0
- painapple_code/static/js/preview-plugins/plugin-helpers.js +282 -0
- painapple_code/static/js/project-color-picker.js +110 -0
- painapple_code/static/js/project-colors.js +163 -0
- painapple_code/static/js/prompt-favorites.js +187 -0
- painapple_code/static/js/quick-actions-menu.js +1736 -0
- painapple_code/static/js/quick-actions-registry.js +1239 -0
- painapple_code/static/js/quick-switcher/controller.js +294 -0
- painapple_code/static/js/quick-switcher/fuzzy-scorer.js +123 -0
- painapple_code/static/js/quick-switcher/index.js +30 -0
- painapple_code/static/js/quick-switcher/providers/base-provider.js +51 -0
- painapple_code/static/js/quick-switcher/providers/command-provider.js +56 -0
- painapple_code/static/js/quick-switcher/providers/file-provider.js +291 -0
- painapple_code/static/js/quick-switcher/providers/panel-provider.js +132 -0
- painapple_code/static/js/quick-switcher/providers/project-provider.js +361 -0
- painapple_code/static/js/quick-switcher/providers/read-files-provider.js +104 -0
- painapple_code/static/js/quick-switcher/providers/skills-provider.js +129 -0
- painapple_code/static/js/quick-switcher/registry.js +69 -0
- painapple_code/static/js/quick-switcher/ui/item.js +54 -0
- painapple_code/static/js/quick-switcher/ui/picker.js +464 -0
- painapple_code/static/js/recent-opens.js +40 -0
- painapple_code/static/js/scroll-manager.js +530 -0
- painapple_code/static/js/scroll-state-machine.js +359 -0
- painapple_code/static/js/selection/action-bar.js +793 -0
- painapple_code/static/js/selection/index.js +15 -0
- painapple_code/static/js/selection/selection-handler.js +877 -0
- painapple_code/static/js/selection/state.js +181 -0
- painapple_code/static/js/session/agent-progress.js +202 -0
- painapple_code/static/js/session/handle-agent-message.js +481 -0
- painapple_code/static/js/session/handle-message.js +550 -0
- painapple_code/static/js/session/interactive.js +587 -0
- painapple_code/static/js/session/message-store.js +271 -0
- painapple_code/static/js/session/messages.js +253 -0
- painapple_code/static/js/session/persistence.js +179 -0
- painapple_code/static/js/session/restore.js +136 -0
- painapple_code/static/js/session/sync.js +333 -0
- painapple_code/static/js/session-container-pool.js +371 -0
- painapple_code/static/js/session-setup-panel.js +274 -0
- painapple_code/static/js/session.js +1047 -0
- painapple_code/static/js/shortcut-hints.js +272 -0
- painapple_code/static/js/shortcuts.js +1146 -0
- painapple_code/static/js/skills-autocomplete.js +262 -0
- painapple_code/static/js/snippets-autocomplete.js +738 -0
- painapple_code/static/js/stash-refs-view.js +83 -0
- painapple_code/static/js/stash-ui.js +768 -0
- painapple_code/static/js/stash.js +785 -0
- painapple_code/static/js/status-bar.js +1184 -0
- painapple_code/static/js/token-profile.js +274 -0
- painapple_code/static/js/tool-renderer-blocks.js +1626 -0
- painapple_code/static/js/tool-renderer-thinking.js +813 -0
- painapple_code/static/js/tool-renderer.js +1500 -0
- painapple_code/static/js/tooltips.js +197 -0
- painapple_code/static/js/upload-manager.js +1019 -0
- painapple_code/static/js/utils.js +521 -0
- painapple_code/static/js/welcome/api.js +363 -0
- painapple_code/static/js/welcome/cards.js +626 -0
- painapple_code/static/js/welcome/context-menu.js +585 -0
- painapple_code/static/js/welcome/families.js +801 -0
- painapple_code/static/js/welcome/preview.js +234 -0
- painapple_code/static/js/welcome/state.js +129 -0
- painapple_code/static/js/welcome.js +1874 -0
- painapple_code/static/js/widget-system/base-widget.js +717 -0
- painapple_code/static/js/widget-system/device-manager.js +178 -0
- painapple_code/static/js/widget-system/event-bus.js +152 -0
- painapple_code/static/js/widget-system/icons.js +120 -0
- painapple_code/static/js/widget-system/index.js +49 -0
- painapple_code/static/js/widget-system/init.js +54 -0
- painapple_code/static/js/widget-system/types/bottom-sheet.js +307 -0
- painapple_code/static/js/widget-system/types/floating.js +915 -0
- painapple_code/static/js/widget-system/types/index.js +40 -0
- painapple_code/static/js/widget-system/types/modal.js +147 -0
- painapple_code/static/js/widget-system/types/sidebar.js +158 -0
- painapple_code/static/js/widget-system/types/tab.js +146 -0
- painapple_code/static/js/widget-system/types/top-sheet.js +203 -0
- painapple_code/static/js/widget-system/widget-manager.js +1187 -0
- painapple_code/static/js/widgets/about-widget.js +260 -0
- painapple_code/static/js/widgets/active-sessions-widget.js +760 -0
- painapple_code/static/js/widgets/agents-widget.js +897 -0
- painapple_code/static/js/widgets/browser-widget.js +579 -0
- painapple_code/static/js/widgets/commands-widget.js +694 -0
- painapple_code/static/js/widgets/config/commit-sections.js +449 -0
- painapple_code/static/js/widgets/config/dir-autocomplete.js +224 -0
- painapple_code/static/js/widgets/config/gestures.js +161 -0
- painapple_code/static/js/widgets/config/models-tab.js +988 -0
- painapple_code/static/js/widgets/config/quick-actions-tab.js +882 -0
- painapple_code/static/js/widgets/config/shortcut-editor.js +587 -0
- painapple_code/static/js/widgets/config/state.js +517 -0
- painapple_code/static/js/widgets/config/system-controls.js +90 -0
- painapple_code/static/js/widgets/config-widget.js +1535 -0
- painapple_code/static/js/widgets/cost-analytics-widget.js +621 -0
- painapple_code/static/js/widgets/debug-widget.js +693 -0
- painapple_code/static/js/widgets/diff-viewer-widget.js +1420 -0
- painapple_code/static/js/widgets/discussion-widget.js +1107 -0
- painapple_code/static/js/widgets/file-explorer-widget.js +2166 -0
- painapple_code/static/js/widgets/file-preview-widget.js +861 -0
- painapple_code/static/js/widgets/git-widget.js +794 -0
- painapple_code/static/js/widgets/helpers-install-widget.js +720 -0
- painapple_code/static/js/widgets/history-explorer-widget.js +1804 -0
- painapple_code/static/js/widgets/image-annotate-widget.js +951 -0
- painapple_code/static/js/widgets/image-preview-widget.js +692 -0
- painapple_code/static/js/widgets/index.js +176 -0
- painapple_code/static/js/widgets/log-explorer-widget.js +1057 -0
- painapple_code/static/js/widgets/plugins-widget.js +305 -0
- painapple_code/static/js/widgets/prompt-explorer-widget.js +1119 -0
- painapple_code/static/js/widgets/search-files-widget.js +529 -0
- painapple_code/static/js/widgets/skills-widget.js +843 -0
- painapple_code/static/js/widgets/snippets-widget.js +479 -0
- painapple_code/static/js/widgets/sub-agents-widget.js +333 -0
- painapple_code/static/js/widgets/tasks-widget.js +379 -0
- painapple_code/static/js/widgets/terminal/connection.js +283 -0
- painapple_code/static/js/widgets/terminal/gestures.js +357 -0
- painapple_code/static/js/widgets/terminal/init.js +642 -0
- painapple_code/static/js/widgets/terminal/link-providers.js +532 -0
- painapple_code/static/js/widgets/terminal/osc52.js +213 -0
- painapple_code/static/js/widgets/terminal/render.js +159 -0
- painapple_code/static/js/widgets/terminal/size.js +54 -0
- painapple_code/static/js/widgets/terminal/state.js +211 -0
- painapple_code/static/js/widgets/terminal/wrap-utils.js +101 -0
- painapple_code/static/js/widgets/terminal-widget.js +793 -0
- painapple_code/static/js/widgets/uploads-widget.js +461 -0
- painapple_code/static/js/widgets/zen-widget.js +1001 -0
- painapple_code/static/login.html +51 -0
- painapple_code/static/manifest.json +73 -0
- painapple_code/static/sw.js +293 -0
- painapple_code/static/vendor/LICENSES.txt +769 -0
- painapple_code/static/vendor/README.md +103 -0
- painapple_code/static/vendor/codemirror.js +35 -0
- painapple_code/static/vendor/codemirror.js.LEGAL.txt +304 -0
- painapple_code/static/vendor/github-markdown-dark.min.css +1124 -0
- painapple_code/static/vendor/highlight-github-dark.min.css +10 -0
- painapple_code/static/vendor/highlight-lang-dockerfile.min.js +8 -0
- painapple_code/static/vendor/highlight-lang-nginx.min.js +21 -0
- painapple_code/static/vendor/highlight-lang-properties.min.js +10 -0
- painapple_code/static/vendor/highlight-lang-scala.min.js +28 -0
- painapple_code/static/vendor/highlight.min.js +1213 -0
- painapple_code/static/vendor/marked.min.js +6 -0
- painapple_code/static/vendor/purify.min.js +3 -0
- painapple_code/static/vendor/xterm-addon-fit.js +2 -0
- painapple_code/static/vendor/xterm.css +209 -0
- painapple_code/static/vendor/xterm.js +2 -0
- painapple_code/static/web-client.html +635 -0
- painapple_code/subprocess_registry.py +535 -0
- painapple_code/tls_cert.py +79 -0
- painapple_code/tools/agents/shadow-git-helper.md +400 -0
- painapple_code/tools/excalidraw-to-svg.js +143 -0
- painapple_code/tools/install-helpers.sh +148 -0
- painapple_code/tools/shadow-git +320 -0
- painapple_code/tools/shadow-query +120 -0
- painapple_code/tools/vegalite-to-svg.js +79 -0
- painapple_code/turn_query.py +766 -0
- painapple_code/turn_tracker.py +221 -0
- painapple_code/utils/__init__.py +0 -0
- painapple_code/utils/agent_cli.py +306 -0
- painapple_code/utils/chart.py +103 -0
- painapple_code/utils/excalidraw.py +107 -0
- painapple_code/utils/file_lock.py +162 -0
- painapple_code/utils/file_paths.py +648 -0
- painapple_code/utils/generate_icons.py +251 -0
- painapple_code/utils/proc.py +205 -0
- painapple_code/utils/pty_backend.py +59 -0
- painapple_code/utils/pty_posix.py +160 -0
- painapple_code/utils/pty_windows.py +245 -0
- painapple_code/utils/token_profiles.py +91 -0
- painapple_code/viewer_templates.py +657 -0
- painapple_code/welcome_search.py +879 -0
- painapple_code-1.0.0.dist-info/METADATA +292 -0
- painapple_code-1.0.0.dist-info/RECORD +436 -0
- painapple_code-1.0.0.dist-info/WHEEL +5 -0
- painapple_code-1.0.0.dist-info/entry_points.txt +2 -0
- painapple_code-1.0.0.dist-info/licenses/LICENSE +661 -0
- painapple_code-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +261 -0
- painapple_code-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Top-level CLI dispatch for the ``painapple`` command.
|
|
2
|
+
|
|
3
|
+
``painapple`` is the sole console script (the old ``painapple-code``
|
|
4
|
+
name was retired; ``python -m painapple_code`` still works everywhere).
|
|
5
|
+
|
|
6
|
+
Backward compatibility is the design constraint: bare invocations like
|
|
7
|
+
``painapple --port 8880 --cwd …`` are baked into systemd units,
|
|
8
|
+
start.sh, deploy.fish, and docker-entrypoint.sh, so they must keep
|
|
9
|
+
working unchanged. Dispatch peeks at the first argument — a known
|
|
10
|
+
subcommand routes to its handler, anything else falls through to the
|
|
11
|
+
server's flat argument parser exactly as before. The one deliberate
|
|
12
|
+
exception: a *leading* ``-h``/``--help`` shows the curated command
|
|
13
|
+
overview below instead of the server's flag dump (``painapple serve
|
|
14
|
+
--help`` still prints the full flag reference).
|
|
15
|
+
|
|
16
|
+
Docker is not a command group — it's a run mode. ``--in-docker`` runs
|
|
17
|
+
the same serve invocation in a container; profiles carry a ``mode:
|
|
18
|
+
host|docker`` and every verb (start/stop/logs/…) dispatches on it.
|
|
19
|
+
The old ``painapple docker`` group prints a "moved" pointer.
|
|
20
|
+
|
|
21
|
+
Subcommands:
|
|
22
|
+
serve explicit alias for the default server invocation
|
|
23
|
+
setup [NAME] global defaults wizard / profile wizard (creates NAME)
|
|
24
|
+
list the fleet view: every deployment + unmanaged processes
|
|
25
|
+
start/stop/restart [NAME] background lifecycle (mode-aware)
|
|
26
|
+
status [NAME] one deployment's detail — bare, it IS the fleet view
|
|
27
|
+
logs [NAME] follow output (host: console.log · docker: container)
|
|
28
|
+
password [NAME] show the server auth URL + password
|
|
29
|
+
pull fetch the prebuilt container image
|
|
30
|
+
shell/extract/claude-login [NAME] docker-mode utilities
|
|
31
|
+
help curated overview (also -h/--help as the first argument)
|
|
32
|
+
version the version string (also -v/--version as a flag)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
SUBCOMMANDS = ("serve", "setup", "list", "start", "stop", "restart",
|
|
38
|
+
"status", "logs", "password", "pull", "shell", "extract",
|
|
39
|
+
"claude-login", "help", "version")
|
|
40
|
+
|
|
41
|
+
_LIST_ALIASES = ("list", "ls", "instances", "profiles")
|
|
42
|
+
_MANAGE_VERBS = {
|
|
43
|
+
"status": "status", "ps": "status",
|
|
44
|
+
"logs": "logs", "log": "logs",
|
|
45
|
+
"password": "password", "show-password": "password",
|
|
46
|
+
"url": "password", "token": "password",
|
|
47
|
+
"pull": "pull",
|
|
48
|
+
"shell": "shell", "sh": "shell",
|
|
49
|
+
"extract": "extract", "export": "extract",
|
|
50
|
+
"claude-login": "claude-login", "login": "claude-login",
|
|
51
|
+
"profile": "profile", # scripted get/set (desktop launcher)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _help():
|
|
56
|
+
"""Command overview — the front door. Full serve-flag reference
|
|
57
|
+
stays on `painapple serve --help` (argparse)."""
|
|
58
|
+
from painapple_code import __version__
|
|
59
|
+
from painapple_code.cli.ui import BOLD, DIM, GREEN, RESET, say
|
|
60
|
+
p = "painapple"
|
|
61
|
+
say(f"""{BOLD}{p}{RESET} — Claude Code in your browser: self-hosted server + web client {DIM}v{__version__}{RESET}
|
|
62
|
+
|
|
63
|
+
{BOLD}Usage:{RESET}
|
|
64
|
+
{p} {DIM}[flags…]{RESET} Serve the current directory {DIM}(same as: {p} serve){RESET}
|
|
65
|
+
{p} {GREEN}--in-docker{RESET} …the same, inside a container {DIM}(Docker/Podman){RESET}
|
|
66
|
+
{p} {GREEN}setup{RESET} Global defaults {DIM}(network · container runtime){RESET}
|
|
67
|
+
{p} {GREEN}setup{RESET} {DIM}NAME{RESET} Create/edit a named deployment {DIM}(host or docker mode){RESET}
|
|
68
|
+
{p} {GREEN}list{RESET} Every instance on this machine
|
|
69
|
+
{p} {GREEN}start{RESET}|{GREEN}stop{RESET}|{GREEN}restart{RESET} {DIM}[NAME]{RESET} Run/stop in the background {DIM}(NAME = profile · label · pid · port){RESET}
|
|
70
|
+
|
|
71
|
+
{BOLD}First run:{RESET}
|
|
72
|
+
cd ~/code/my-project && {p} {DIM}# → http://127.0.0.1:8765 — login URL printed{RESET}
|
|
73
|
+
{p} {GREEN}pull{RESET} && {p} {GREEN}--in-docker{RESET} {DIM}# same, sandboxed in the prebuilt image{RESET}
|
|
74
|
+
|
|
75
|
+
{BOLD}Common serve flags:{RESET} {DIM}(all of them: {p} serve --help){RESET}
|
|
76
|
+
--port N Port to bind {DIM}(default 8765){RESET}
|
|
77
|
+
--host IP Host interface {DIM}(default 127.0.0.1 · 0.0.0.0 = reachable on your LAN){RESET}
|
|
78
|
+
--workspace PATH Root dir holding your projects {DIM}(default: current dir){RESET}
|
|
79
|
+
--tls auto|on|off TLS {DIM}(auto = on for non-loopback binds; self-signed cert){RESET}
|
|
80
|
+
--in-docker Run it containerized instead
|
|
81
|
+
|
|
82
|
+
{BOLD}Profiles (multiple deployments):{RESET}
|
|
83
|
+
{p} {GREEN}setup{RESET} work {DIM}# create 'work' — pick host mode or docker mode{RESET}
|
|
84
|
+
{p} {GREEN}start{RESET} work {DIM}# run it in the background — own port/data, isolated{RESET}
|
|
85
|
+
{p} {GREEN}restart{RESET} work · {GREEN}stop{RESET} work {DIM}# manage it ({p} --profile work runs it in the foreground){RESET}
|
|
86
|
+
{p} {GREEN}status{RESET} work · {GREEN}logs{RESET} work · {GREEN}password{RESET} work
|
|
87
|
+
|
|
88
|
+
{BOLD}Everything running:{RESET}
|
|
89
|
+
{p} {GREEN}list{RESET} {DIM}(= {p} {GREEN}status{RESET}{DIM}){RESET} Deployments (host + docker) and unmanaged processes
|
|
90
|
+
{p} {GREEN}status{RESET} {DIM}NAME{RESET} One deployment in detail {DIM}(NAME = profile · label · pid · port){RESET}
|
|
91
|
+
|
|
92
|
+
{BOLD}Container extras:{RESET}
|
|
93
|
+
{p} {GREEN}pull{RESET} [TAG] Fetch the prebuilt image
|
|
94
|
+
{p} {GREEN}shell{RESET}|{GREEN}extract{RESET}|{GREEN}claude-login{RESET} {DIM}[NAME]{RESET} Inside-the-sandbox utilities
|
|
95
|
+
{p} {GREEN}password{RESET}|{GREEN}logs{RESET} {GREEN}--in-docker{RESET} {DIM}…for the ad-hoc sandbox (it has no NAME){RESET}
|
|
96
|
+
|
|
97
|
+
{BOLD}Docs:{RESET} https://painapple.ai/""")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _docker_moved():
|
|
102
|
+
from painapple_code.cli.ui import DIM, GREEN, RESET, err, say
|
|
103
|
+
err("`painapple docker` moved — docker is a run mode now, not a "
|
|
104
|
+
"command group.")
|
|
105
|
+
say(f"""
|
|
106
|
+
painapple {GREEN}--in-docker{RESET} {DIM}run the current dir in a container (was: docker quick){RESET}
|
|
107
|
+
painapple {GREEN}setup NAME{RESET} {DIM}create a docker-mode profile (was: docker setup){RESET}
|
|
108
|
+
painapple {GREEN}start{RESET}|{GREEN}stop{RESET}|{GREEN}restart NAME{RESET} {DIM}(was: docker up -d / stop / restart){RESET}
|
|
109
|
+
painapple {GREEN}pull{RESET} · {GREEN}logs{RESET} · {GREEN}password{RESET} · {GREEN}status{RESET} · {GREEN}shell{RESET} · {GREEN}extract{RESET} · {GREEN}claude-login{RESET} {DIM}[NAME]{RESET}
|
|
110
|
+
painapple {GREEN}list{RESET} {DIM}every deployment (was: docker list){RESET}
|
|
111
|
+
|
|
112
|
+
{DIM}Existing docker profiles are adopted automatically on the next
|
|
113
|
+
profile-aware command (painapple list).{RESET}""")
|
|
114
|
+
return 2
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main(argv=None):
|
|
118
|
+
if sys.platform == "win32": # cp1252 console would crash on → / ╔ in help & banner
|
|
119
|
+
from painapple_code.utils.proc import force_utf8_stdio
|
|
120
|
+
force_utf8_stdio()
|
|
121
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
122
|
+
|
|
123
|
+
if argv and argv[0] in ("help", "-h", "--help"):
|
|
124
|
+
return _help()
|
|
125
|
+
|
|
126
|
+
if argv and argv[0] in ("version", "--version", "-v"):
|
|
127
|
+
# Verb form of the flag — same string, no server import.
|
|
128
|
+
from painapple_code import __version__
|
|
129
|
+
print(f"painapple {__version__}")
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
if argv and argv[0] == "docker":
|
|
133
|
+
return _docker_moved()
|
|
134
|
+
|
|
135
|
+
if argv and argv[0] == "setup":
|
|
136
|
+
from painapple_code.cli.setup_cmd import main as setup_main
|
|
137
|
+
return setup_main(argv[1:])
|
|
138
|
+
|
|
139
|
+
if argv and argv[0] in _LIST_ALIASES:
|
|
140
|
+
from painapple_code.cli.list_cmd import main as list_main
|
|
141
|
+
return list_main(argv[1:])
|
|
142
|
+
|
|
143
|
+
if argv and argv[0] in ("start", "stop", "restart"):
|
|
144
|
+
from painapple_code.cli.lifecycle_cmd import main as lifecycle_main
|
|
145
|
+
return lifecycle_main(argv[0], argv[1:])
|
|
146
|
+
|
|
147
|
+
if argv and argv[0] in _MANAGE_VERBS:
|
|
148
|
+
from painapple_code.cli.manage_cmd import main as manage_main
|
|
149
|
+
return manage_main(_MANAGE_VERBS[argv[0]], argv[1:])
|
|
150
|
+
|
|
151
|
+
if argv and argv[0] == "serve":
|
|
152
|
+
argv = argv[1:]
|
|
153
|
+
|
|
154
|
+
# Optional --profile NAME: a named deployment run in the foreground.
|
|
155
|
+
# Stripped here so the flags-only serve parser never sees it.
|
|
156
|
+
# serve_config is import-light (no yaml/fastapi).
|
|
157
|
+
from painapple_code.cli import serve_config
|
|
158
|
+
try:
|
|
159
|
+
prof, argv = serve_config.extract_profile(argv)
|
|
160
|
+
except ValueError as e:
|
|
161
|
+
from painapple_code.cli.ui import err
|
|
162
|
+
err(str(e))
|
|
163
|
+
return 2
|
|
164
|
+
|
|
165
|
+
# Fast gate: validate flags BEFORE paying the ~300ms server import —
|
|
166
|
+
# bad flags exit 2, -v / --help exit 0, all in ~40ms. Import stays
|
|
167
|
+
# inside this branch so setup/list/help skip it.
|
|
168
|
+
from painapple_code.cli.serve_args import build_parser
|
|
169
|
+
ns = build_parser().parse_args(argv)
|
|
170
|
+
|
|
171
|
+
if prof:
|
|
172
|
+
# Named deployment: adopt legacy stores, then dispatch on mode.
|
|
173
|
+
from painapple_code.cli import profiles
|
|
174
|
+
try:
|
|
175
|
+
profiles.ensure_migrated()
|
|
176
|
+
loaded = profiles.load(prof)
|
|
177
|
+
if loaded is not None and (loaded.is_docker or ns.in_docker):
|
|
178
|
+
from painapple_code.cli.deploy.launch import run_profile
|
|
179
|
+
try:
|
|
180
|
+
return run_profile(loaded, argv)
|
|
181
|
+
except KeyboardInterrupt:
|
|
182
|
+
return 130
|
|
183
|
+
if loaded is None and ns.in_docker:
|
|
184
|
+
from painapple_code.cli.ui import err
|
|
185
|
+
err(f"Profile {prof!r} doesn't exist — create it with "
|
|
186
|
+
f"`painapple setup {prof}`, or drop --profile for an "
|
|
187
|
+
"ad-hoc --in-docker run.")
|
|
188
|
+
return 2
|
|
189
|
+
profiles.activate(prof)
|
|
190
|
+
except ValueError as e:
|
|
191
|
+
from painapple_code.cli.ui import err
|
|
192
|
+
err(str(e))
|
|
193
|
+
return 2
|
|
194
|
+
elif ns.in_docker:
|
|
195
|
+
from painapple_code.cli.deploy.launch import run_adhoc
|
|
196
|
+
try:
|
|
197
|
+
return run_adhoc(argv)
|
|
198
|
+
except KeyboardInterrupt:
|
|
199
|
+
return 130
|
|
200
|
+
|
|
201
|
+
# Parse succeeded → boot. server.main() re-parses with the same
|
|
202
|
+
# builder (~1ms) so its signature and behavior stay unchanged.
|
|
203
|
+
from painapple_code.server import main as serve_main
|
|
204
|
+
return serve_main(argv)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Container run-mode machinery (Docker/Podman).
|
|
2
|
+
|
|
3
|
+
Docker is not a separate command group — it's *how* a deployment runs.
|
|
4
|
+
This package holds everything the unified CLI needs to run a workspace
|
|
5
|
+
or a profile inside a container:
|
|
6
|
+
|
|
7
|
+
* ``runtime`` — docker/podman resolution + invocation helpers
|
|
8
|
+
* ``config`` — DockerSettings: the docker-mode key model + validation
|
|
9
|
+
* ``container`` — run/stop/logs/shell/password/extract/pull bodies
|
|
10
|
+
* ``claude_seed`` — .claude credential/onboarding seeding for sandboxes
|
|
11
|
+
|
|
12
|
+
Entry points: ``painapple --in-docker`` (cwd sandbox), docker-mode
|
|
13
|
+
profiles (``painapple setup NAME`` → ``painapple start NAME``), and the
|
|
14
|
+
management verbs (``painapple logs/password/shell/extract NAME``).
|
|
15
|
+
"""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Seeding an isolated CLAUDE_HOME for container sandboxes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Subset of ~/.claude.json worth carrying into the container: "not a
|
|
7
|
+
# fresh install" flags so the onboarding wizard doesn't re-run. The
|
|
8
|
+
# host file's `projects` map is keyed by host paths (dead in the
|
|
9
|
+
# container) and caches refetch on first run — deliberately skipped.
|
|
10
|
+
# KEEP IN SYNC with CLAUDE_JSON_ALLOW in
|
|
11
|
+
# tauri-app/src-tauri/src/local.rs (the desktop app's setup wizard
|
|
12
|
+
# seeds an isolated CLAUDE_HOME the same way).
|
|
13
|
+
CLAUDE_JSON_ALLOW = {
|
|
14
|
+
"hasCompletedOnboarding",
|
|
15
|
+
"lastOnboardingVersion",
|
|
16
|
+
"installMethod",
|
|
17
|
+
"migrationVersion",
|
|
18
|
+
"claudeCodeFirstTokenDate",
|
|
19
|
+
"opusProMigrationComplete",
|
|
20
|
+
"sonnet1m45MigrationComplete",
|
|
21
|
+
"lastReleaseNotesSeen",
|
|
22
|
+
"opus47LaunchSeenCount",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def seed_claude_json(src, dst):
|
|
27
|
+
"""Write a minimal .claude.json (dst) from the allowlisted fields of
|
|
28
|
+
the host file (src). Returns False on any parse/write problem."""
|
|
29
|
+
try:
|
|
30
|
+
from painapple_code.paths import lock_mode
|
|
31
|
+
|
|
32
|
+
data = json.loads(Path(src).read_text(encoding="utf-8"))
|
|
33
|
+
subset = {k: data[k] for k in CLAUDE_JSON_ALLOW if k in data}
|
|
34
|
+
Path(dst).write_text(json.dumps(subset, indent=2), encoding="utf-8")
|
|
35
|
+
# lock_mode, not chmod: on Windows chmod only toggles the
|
|
36
|
+
# read-only attribute and reports success, so the 0600 here was
|
|
37
|
+
# decorative and this file inherited whatever ACEs its parent had.
|
|
38
|
+
lock_mode(dst, 0o600)
|
|
39
|
+
return True
|
|
40
|
+
except (OSError, ValueError):
|
|
41
|
+
return False
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Docker-mode settings: the key model + validation for containerized
|
|
2
|
+
deployments.
|
|
3
|
+
|
|
4
|
+
There is no separate docker config file anymore. Docker-mode settings
|
|
5
|
+
live in the unified profile store (``profiles/NAME/profile.yaml`` with
|
|
6
|
+
``mode: docker`` — see cli/profiles.py), and the ad-hoc
|
|
7
|
+
``painapple --in-docker`` run assembles its settings from the global
|
|
8
|
+
serve defaults (serve.yaml ``runtime``/``runtime_flags``/``image``)
|
|
9
|
+
plus the serve flags of that invocation.
|
|
10
|
+
|
|
11
|
+
``DockerSettings`` keeps the KEY-based ``assign()`` validation so
|
|
12
|
+
scripted writers (``painapple profile set``) and the desktop launcher
|
|
13
|
+
accept/reject the same inputs the wizard does.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from painapple_code.cli.serve_config import root_home
|
|
20
|
+
|
|
21
|
+
UPSTREAM_IMAGE = "wrotek/painapple-code"
|
|
22
|
+
DEFAULT_IMAGE = "painapple-code:latest"
|
|
23
|
+
|
|
24
|
+
WORKSPACE_MODES = ("project", "parent", "multi")
|
|
25
|
+
TLS_MODES = ("auto", "on", "off")
|
|
26
|
+
ACCENT_NAMES = ("blue", "green", "red", "orange", "purple", "cyan")
|
|
27
|
+
_HEX_RE = re.compile(r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
|
28
|
+
_IPV4_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$")
|
|
29
|
+
|
|
30
|
+
# Order = order printed by scripted `get` with no args.
|
|
31
|
+
CONFIG_KEYS = (
|
|
32
|
+
"RUNTIME", "RUNTIME_FLAGS", "WORKSPACE", "WORKSPACE_MODE",
|
|
33
|
+
"CLAUDE_HOME", "CLAUDE_JSON", "DATA_VOLUME", "CONFIG_VOLUME",
|
|
34
|
+
"IMAGE", "CONTAINER", "PORT", "LISTEN_HOST", "TLS_MODE",
|
|
35
|
+
"INSTANCE_NAME", "ACCENT",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# profile.yaml uses the shared serve-key names where a docker setting
|
|
39
|
+
# is the same concept: LISTEN_HOST ↔ host (the bind), TLS_MODE ↔ tls.
|
|
40
|
+
_YAML_NAME = {"LISTEN_HOST": "host", "TLS_MODE": "tls"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _expand(value):
|
|
44
|
+
if value in ("~",) or value.startswith("~/"):
|
|
45
|
+
return str(Path(value).expanduser())
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ConfigError(ValueError):
|
|
50
|
+
"""Bad key or value in a settings assignment. Message is user-facing."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DockerSettings:
|
|
54
|
+
"""One containerized deployment's settings, with per-profile
|
|
55
|
+
collision-free defaults (container name, data volume, global config
|
|
56
|
+
dir). CLAUDE_HOME defaults to the shared isolated dir for every
|
|
57
|
+
profile, so one ``claude login`` serves all sandboxes."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, profile=None):
|
|
60
|
+
home = root_home()
|
|
61
|
+
shared = home / "shared"
|
|
62
|
+
suffix = f"-{profile}" if profile else ""
|
|
63
|
+
cfg_dirname = f"docker-{profile}" if profile else "shared"
|
|
64
|
+
self.runtime = "" # empty = auto-detect; may be a binary path
|
|
65
|
+
self.runtime_flags = ""
|
|
66
|
+
self.workspace = ""
|
|
67
|
+
self.workspace_mode = "project"
|
|
68
|
+
self.workspaces = [] # multi mode only
|
|
69
|
+
self.claude_home = str(shared / ".claude")
|
|
70
|
+
self.claude_json = "" # empty = derive ${claude_home}.json
|
|
71
|
+
self.data_volume = f"painapple-data{suffix}"
|
|
72
|
+
self.config_volume = str(
|
|
73
|
+
Path(f"~/.config/painapple-code/{cfg_dirname}").expanduser())
|
|
74
|
+
self.image = DEFAULT_IMAGE
|
|
75
|
+
self.container = f"painapple-code{suffix}"
|
|
76
|
+
self.port = 8765
|
|
77
|
+
self.listen_host = "127.0.0.1"
|
|
78
|
+
self.tls_mode = "auto"
|
|
79
|
+
self.instance_name = ""
|
|
80
|
+
self.accent = ""
|
|
81
|
+
|
|
82
|
+
# ── Derived values ──────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
def effective_claude_json(self):
|
|
85
|
+
return self.claude_json or f"{self.claude_home}.json"
|
|
86
|
+
|
|
87
|
+
def effective_tls(self):
|
|
88
|
+
"""Resolve tls_mode 'auto' against the real reachability
|
|
89
|
+
(listen_host), since the server always binds 0.0.0.0 in-container."""
|
|
90
|
+
if self.tls_mode != "auto":
|
|
91
|
+
return self.tls_mode
|
|
92
|
+
return "off" if self.listen_host in ("127.0.0.1", "::1", "localhost") else "on"
|
|
93
|
+
|
|
94
|
+
def data_is_bind(self):
|
|
95
|
+
return Path(self.data_volume).is_absolute()
|
|
96
|
+
|
|
97
|
+
def config_is_bind(self):
|
|
98
|
+
return Path(self.config_volume).is_absolute()
|
|
99
|
+
|
|
100
|
+
def container_mount(self):
|
|
101
|
+
"""In-container mount path — the path hash keys project identity.
|
|
102
|
+
|
|
103
|
+
The server itself always serves ``--workspace /workspace`` (the dir
|
|
104
|
+
that HOLDS projects); modes only differ in what lands where. Project
|
|
105
|
+
mode therefore mounts the single repo one level down — /workspace
|
|
106
|
+
acts as a synthetic parent, so the welcome screen offers the repo as
|
|
107
|
+
one pickable project instead of listing its own subdirs as projects.
|
|
108
|
+
"""
|
|
109
|
+
if self.workspace_mode in ("parent", "multi"):
|
|
110
|
+
return "/workspace"
|
|
111
|
+
return f"/workspace/{Path(self.workspace).name}"
|
|
112
|
+
|
|
113
|
+
# ── get/set by KEY ──────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
_FIELD_BY_KEY = {k: k.lower() for k in CONFIG_KEYS}
|
|
116
|
+
|
|
117
|
+
def get(self, key):
|
|
118
|
+
if key not in CONFIG_KEYS:
|
|
119
|
+
raise ConfigError(f"Unknown key: {key}")
|
|
120
|
+
value = getattr(self, self._FIELD_BY_KEY[key])
|
|
121
|
+
return str(value)
|
|
122
|
+
|
|
123
|
+
def assign(self, key, value):
|
|
124
|
+
"""Validate and set one field. Raises ConfigError with a
|
|
125
|
+
user-facing message on bad key/value."""
|
|
126
|
+
if key == "RUNTIME":
|
|
127
|
+
if value and value not in ("docker", "podman"):
|
|
128
|
+
# Custom binary: an explicit path must exist + be a file.
|
|
129
|
+
expanded = _expand(value)
|
|
130
|
+
if not (Path(expanded).is_absolute() and Path(expanded).is_file()):
|
|
131
|
+
raise ConfigError(
|
|
132
|
+
"RUNTIME must be 'docker', 'podman', an absolute path "
|
|
133
|
+
f"to a runtime binary, or empty to auto-detect: {value}")
|
|
134
|
+
value = expanded
|
|
135
|
+
self.runtime = value
|
|
136
|
+
elif key == "RUNTIME_FLAGS":
|
|
137
|
+
self.runtime_flags = value
|
|
138
|
+
elif key == "WORKSPACE":
|
|
139
|
+
value = _expand(value)
|
|
140
|
+
if value and not Path(value).is_dir():
|
|
141
|
+
raise ConfigError(f"WORKSPACE does not exist: {value}")
|
|
142
|
+
self.workspace = str(Path(value).resolve()) if value else ""
|
|
143
|
+
elif key == "WORKSPACE_MODE":
|
|
144
|
+
if value not in WORKSPACE_MODES:
|
|
145
|
+
raise ConfigError(
|
|
146
|
+
f"WORKSPACE_MODE must be 'project', 'parent', or 'multi': {value}")
|
|
147
|
+
self.workspace_mode = value
|
|
148
|
+
elif key == "CLAUDE_HOME":
|
|
149
|
+
value = _expand(value)
|
|
150
|
+
if not value:
|
|
151
|
+
raise ConfigError("CLAUDE_HOME can't be empty")
|
|
152
|
+
# If CLAUDE_JSON was implicitly tracking the previous
|
|
153
|
+
# CLAUDE_HOME, reset it so it re-derives against the new one.
|
|
154
|
+
if self.claude_json == f"{self.claude_home}.json":
|
|
155
|
+
self.claude_json = ""
|
|
156
|
+
self.claude_home = value
|
|
157
|
+
elif key == "CLAUDE_JSON":
|
|
158
|
+
# Empty is meaningful: "derive from CLAUDE_HOME".
|
|
159
|
+
self.claude_json = _expand(value)
|
|
160
|
+
elif key in ("DATA_VOLUME", "CONFIG_VOLUME"):
|
|
161
|
+
value = _expand(value)
|
|
162
|
+
if not value:
|
|
163
|
+
raise ConfigError(f"{key} can't be empty")
|
|
164
|
+
setattr(self, key.lower(), value)
|
|
165
|
+
elif key in ("IMAGE", "CONTAINER"):
|
|
166
|
+
if not value:
|
|
167
|
+
raise ConfigError(f"{key} can't be empty")
|
|
168
|
+
setattr(self, key.lower(), value)
|
|
169
|
+
elif key == "PORT":
|
|
170
|
+
if not value.isdigit() or not 1 <= int(value) <= 65535:
|
|
171
|
+
raise ConfigError(f"PORT must be 1–65535: {value}")
|
|
172
|
+
self.port = int(value)
|
|
173
|
+
elif key == "LISTEN_HOST":
|
|
174
|
+
if value == "localhost":
|
|
175
|
+
value = "127.0.0.1"
|
|
176
|
+
if not _IPV4_RE.match(value):
|
|
177
|
+
raise ConfigError(
|
|
178
|
+
f"LISTEN_HOST must be an IPv4 address (or 'localhost'): {value}")
|
|
179
|
+
self.listen_host = value
|
|
180
|
+
elif key == "TLS_MODE":
|
|
181
|
+
if value not in TLS_MODES:
|
|
182
|
+
raise ConfigError(f"TLS_MODE must be 'auto', 'on', or 'off': {value}")
|
|
183
|
+
self.tls_mode = value
|
|
184
|
+
elif key == "INSTANCE_NAME":
|
|
185
|
+
if len(value) > 12:
|
|
186
|
+
raise ConfigError(f"INSTANCE_NAME too long ({len(value)} chars, max 12)")
|
|
187
|
+
self.instance_name = value
|
|
188
|
+
elif key == "ACCENT":
|
|
189
|
+
if value and value not in ACCENT_NAMES and not _HEX_RE.match(value):
|
|
190
|
+
raise ConfigError(
|
|
191
|
+
"ACCENT must be empty, a named color "
|
|
192
|
+
f"({'|'.join(ACCENT_NAMES)}), or hex (#RGB / #RRGGBB)")
|
|
193
|
+
self.accent = value
|
|
194
|
+
else:
|
|
195
|
+
raise ConfigError(f"Unknown key: {key}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def flag_to_key(flag):
|
|
199
|
+
"""--listen-host → LISTEN_HOST. Returns None for unknown/non-flags."""
|
|
200
|
+
if not flag.startswith("--"):
|
|
201
|
+
return None
|
|
202
|
+
key = flag[2:].replace("-", "_").upper()
|
|
203
|
+
return key if key in CONFIG_KEYS else None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ──── profile.yaml (docker mode) mapping ─────────────────────────────────
|
|
207
|
+
|
|
208
|
+
def settings_to_data(cfg):
|
|
209
|
+
"""DockerSettings → the docker-mode key subset of profile.yaml
|
|
210
|
+
(shared names for shared concepts: host = bind, tls = tls mode)."""
|
|
211
|
+
return {
|
|
212
|
+
"runtime": cfg.runtime,
|
|
213
|
+
"runtime_flags": cfg.runtime_flags,
|
|
214
|
+
"workspace": cfg.workspace,
|
|
215
|
+
"workspace_mode": cfg.workspace_mode,
|
|
216
|
+
"workspaces": list(cfg.workspaces),
|
|
217
|
+
"claude_home": cfg.claude_home,
|
|
218
|
+
"claude_json": cfg.claude_json,
|
|
219
|
+
"data_volume": cfg.data_volume,
|
|
220
|
+
"config_volume": cfg.config_volume,
|
|
221
|
+
"image": cfg.image,
|
|
222
|
+
"container": cfg.container,
|
|
223
|
+
"port": cfg.port,
|
|
224
|
+
"host": cfg.listen_host,
|
|
225
|
+
"tls": cfg.tls_mode,
|
|
226
|
+
"instance_name": cfg.instance_name,
|
|
227
|
+
"accent": cfg.accent,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def settings_from_data(data, profile=None):
|
|
232
|
+
"""profile.yaml data (or a legacy docker.yaml dict) → DockerSettings.
|
|
233
|
+
Accepts both the unified names (host/tls) and the legacy ones
|
|
234
|
+
(listen_host/tls_mode). Stale values (e.g. a WORKSPACE dir that no
|
|
235
|
+
longer exists) are kept verbatim so status/list still show them and
|
|
236
|
+
an actual start fails with a clear error."""
|
|
237
|
+
data = data or {}
|
|
238
|
+
cfg = DockerSettings(profile=profile)
|
|
239
|
+
aliases = {"host": "LISTEN_HOST", "listen_host": "LISTEN_HOST",
|
|
240
|
+
"tls": "TLS_MODE", "tls_mode": "TLS_MODE"}
|
|
241
|
+
for name, raw in data.items():
|
|
242
|
+
if name == "workspaces":
|
|
243
|
+
if isinstance(raw, list):
|
|
244
|
+
cfg.workspaces = [str(w) for w in raw]
|
|
245
|
+
continue
|
|
246
|
+
key = aliases.get(name) or (name.upper() if name.upper() in CONFIG_KEYS
|
|
247
|
+
else None)
|
|
248
|
+
if key is None or raw in (None, ""):
|
|
249
|
+
continue
|
|
250
|
+
try:
|
|
251
|
+
cfg.assign(key, str(raw))
|
|
252
|
+
except ConfigError:
|
|
253
|
+
if key == "PORT":
|
|
254
|
+
try:
|
|
255
|
+
cfg.port = int(raw)
|
|
256
|
+
except (TypeError, ValueError):
|
|
257
|
+
pass
|
|
258
|
+
else:
|
|
259
|
+
setattr(cfg, key.lower(), str(raw))
|
|
260
|
+
return cfg
|