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