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,572 @@
|
|
|
1
|
+
"""The fleet view — every instance on this machine, local + docker.
|
|
2
|
+
|
|
3
|
+
Reached as ``painapple list`` (and its aliases) or as a bare
|
|
4
|
+
``painapple status``; ``painapple status NAME`` is the per-deployment
|
|
5
|
+
detail block in manage_cmd. One renderer, two entry points.
|
|
6
|
+
|
|
7
|
+
Two sections, and the split is the point:
|
|
8
|
+
|
|
9
|
+
* **Deployments** — the things the verbs target: the flag-less root
|
|
10
|
+
deployment (``default``) plus every saved profile, each matched to a
|
|
11
|
+
running process (host) or container (docker).
|
|
12
|
+
* **Unmanaged processes** — painapple servers in the process table that
|
|
13
|
+
belong to NO deployment: launched by hand or by a service unit with
|
|
14
|
+
their own flags. Their name is just their ``--instance-name`` label.
|
|
15
|
+
|
|
16
|
+
Local servers have no registry — a bare ``painapple [serve]`` process IS
|
|
17
|
+
the instance — so they're discovered by scanning the process table and
|
|
18
|
+
parsing the serve flags out of each command line.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from painapple_code.cli.ui import BOLD, DIM, GREEN, RESET, say
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ──── Local server discovery (process scan) ──────────────────────────────
|
|
30
|
+
|
|
31
|
+
def _iter_processes():
|
|
32
|
+
"""(pid, argv_tokens) for every visible process.
|
|
33
|
+
|
|
34
|
+
psutil rather than `ps ax`: there is no ps on Windows, and psutil
|
|
35
|
+
hands back a real argv LIST instead of a line we have to re-split on
|
|
36
|
+
spaces — which is what made `C:\\Program Files\\...\\painapple.exe`
|
|
37
|
+
(or any macOS path with a space) unparseable.
|
|
38
|
+
"""
|
|
39
|
+
import psutil
|
|
40
|
+
for proc in psutil.process_iter(["pid", "cmdline"]):
|
|
41
|
+
try:
|
|
42
|
+
argv = proc.info["cmdline"]
|
|
43
|
+
if argv:
|
|
44
|
+
yield proc.info["pid"], list(argv)
|
|
45
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _proc_environ(pid):
|
|
50
|
+
"""A process's environment as a dict, or None when unreadable.
|
|
51
|
+
|
|
52
|
+
Works for same-user processes on Linux, macOS AND Windows — the
|
|
53
|
+
/proc/<pid>/environ reads this replaces were Linux-only, which is why
|
|
54
|
+
macOS silently fell back to port-only matching (and why `painapple
|
|
55
|
+
stop` on Windows could kill whatever else held the port).
|
|
56
|
+
"""
|
|
57
|
+
import psutil
|
|
58
|
+
try:
|
|
59
|
+
return psutil.Process(pid).environ()
|
|
60
|
+
except Exception:
|
|
61
|
+
# NoSuchProcess / AccessDenied / NotImplementedError — all mean
|
|
62
|
+
# "not ours to read", which callers already handle.
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _basename(tok):
|
|
67
|
+
"""Last path component, splitting on BOTH separators.
|
|
68
|
+
|
|
69
|
+
`Path(tok).name` only knows the running platform's separator, so a
|
|
70
|
+
Windows argv0 read on any other host (and, more usefully, in a test)
|
|
71
|
+
comes back whole. Splitting on both keeps the argv-shape checks below
|
|
72
|
+
platform-independent.
|
|
73
|
+
"""
|
|
74
|
+
return re.split(r"[\\/]", tok)[-1]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_python(tok):
|
|
78
|
+
"""Does this argv[0] name a Python interpreter? Case-INSENSITIVE on
|
|
79
|
+
purpose: a Homebrew/python.org framework build's binary is literally
|
|
80
|
+
named `Python` (…/Python.framework/…/Python.app/Contents/MacOS/Python),
|
|
81
|
+
and that's the shebang interpreter pipx bakes into the console script
|
|
82
|
+
on macOS. A case-sensitive test made every pipx-installed `painapple`
|
|
83
|
+
on macOS invisible to the scan — `painapple list` reported the
|
|
84
|
+
deployment stopped while the server was serving."""
|
|
85
|
+
return _basename(tok).lower().startswith("python")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _script_name(tok):
|
|
89
|
+
"""argv[0]'s bare command name, minus a Windows executable suffix."""
|
|
90
|
+
name = _basename(tok).lower()
|
|
91
|
+
for suffix in (".exe", ".cmd", ".bat"):
|
|
92
|
+
if name.endswith(suffix):
|
|
93
|
+
return name[: -len(suffix)]
|
|
94
|
+
return name
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _serve_args(tokens):
|
|
98
|
+
"""If `tokens` (an argv list) is a painapple SERVER process, return
|
|
99
|
+
its flag tail; None for anything else (other programs, subcommand
|
|
100
|
+
invocations like `painapple start work`, this very `painapple list`).
|
|
101
|
+
|
|
102
|
+
argv list only — the ps-based string callers are gone, and re-splitting
|
|
103
|
+
a joined command on spaces is precisely what broke paths containing
|
|
104
|
+
one (`C:\\Program Files\\…\\painapple.exe`).
|
|
105
|
+
"""
|
|
106
|
+
rest = None
|
|
107
|
+
for i, tok in enumerate(tokens):
|
|
108
|
+
# `python [-u …] -m painapple_code` — the interpreter must be
|
|
109
|
+
# argv[0]. Without that anchor, a WRAPPER whose argument text
|
|
110
|
+
# merely contains the string (`sh -c 'python -m painapple_code
|
|
111
|
+
# --port …'`, an editor, a pager) ghosts in as a second row for
|
|
112
|
+
# the same instance.
|
|
113
|
+
if (tok == "painapple_code" and i >= 2 and tokens[i - 1] == "-m"
|
|
114
|
+
and _is_python(tokens[0])
|
|
115
|
+
and all(t.startswith("-") for t in tokens[1:i - 1])):
|
|
116
|
+
rest = tokens[i + 1:]
|
|
117
|
+
break
|
|
118
|
+
# Console script — as argv[0] or behind its shebang interpreter,
|
|
119
|
+
# possibly with interpreter options (`/path/python -E /path/bin/
|
|
120
|
+
# painapple --port …`). The interpreter guard keeps e.g.
|
|
121
|
+
# `grep foo …/painapple-code` from matching.
|
|
122
|
+
if (_script_name(tok) in ("painapple", "painapple-code")
|
|
123
|
+
and (i == 0
|
|
124
|
+
or (_is_python(tokens[0])
|
|
125
|
+
and all(t.startswith("-") for t in tokens[1:i])))):
|
|
126
|
+
rest = tokens[i + 1:]
|
|
127
|
+
break
|
|
128
|
+
if rest is None:
|
|
129
|
+
return None
|
|
130
|
+
# The server parser is flags-only; a leading non-flag is a subcommand.
|
|
131
|
+
if rest and not rest[0].startswith("-"):
|
|
132
|
+
if rest[0] != "serve":
|
|
133
|
+
return None
|
|
134
|
+
rest = rest[1:]
|
|
135
|
+
return rest
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _flag(rest, *names, default=None):
|
|
139
|
+
for i, tok in enumerate(rest):
|
|
140
|
+
for name in names:
|
|
141
|
+
if tok == name and i + 1 < len(rest):
|
|
142
|
+
return rest[i + 1]
|
|
143
|
+
if tok.startswith(name + "="):
|
|
144
|
+
return tok.split("=", 1)[1]
|
|
145
|
+
return default
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _in_container(pid):
|
|
149
|
+
"""Linux: rootless-podman containers show up in the host's process
|
|
150
|
+
table — filter them out (they're the docker section's rows). macOS
|
|
151
|
+
containers live in a VM and never appear, so no check needed.
|
|
152
|
+
|
|
153
|
+
Environment markers first: our own image sets PAINAPPLE_IN_CONTAINER,
|
|
154
|
+
podman sets `container=podman`. The /proc/<pid>/root check is the
|
|
155
|
+
fallback — under --userns=keep-id that link reads back as '/' from
|
|
156
|
+
the host, which used to list every containerized server twice (once
|
|
157
|
+
as a phantom ad-hoc process on the container's own port)."""
|
|
158
|
+
env = _proc_environ(pid)
|
|
159
|
+
if env and ("PAINAPPLE_IN_CONTAINER" in env or "container" in env):
|
|
160
|
+
return True
|
|
161
|
+
if not sys.platform.startswith("linux"):
|
|
162
|
+
return False
|
|
163
|
+
try:
|
|
164
|
+
return os.readlink(f"/proc/{pid}/root") != "/"
|
|
165
|
+
except OSError:
|
|
166
|
+
return False # other user's process — can't tell, assume host
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _profile_of(rest, env):
|
|
170
|
+
"""The profile name this process was launched under, '' when none.
|
|
171
|
+
|
|
172
|
+
argv FIRST, because `profiles.activate()` repoints the data home
|
|
173
|
+
IN-PROCESS: a foreground `painapple --profile NAME` is exec'd with
|
|
174
|
+
neither PAINAPPLE_CODE_HOME nor PAINAPPLE_PROFILE in its environment,
|
|
175
|
+
and psutil hands back the environment it was exec'd with — so the
|
|
176
|
+
only surviving evidence is the flag itself. Without this the instance
|
|
177
|
+
reads back as the ROOT deployment (port 8765, root serve.yaml): its
|
|
178
|
+
own profile row shows `stopped` while the process it can't see holds
|
|
179
|
+
the port, and `painapple stop` aims at it as if it were `default`.
|
|
180
|
+
The env var is the fallback for a spawner that exported it
|
|
181
|
+
(`painapple start NAME`, a service unit)."""
|
|
182
|
+
name = _flag(rest, "--profile", default="")
|
|
183
|
+
if not name:
|
|
184
|
+
name = (env or {}).get("PAINAPPLE_PROFILE", "")
|
|
185
|
+
return name.strip()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _home_of(pid, rest=()):
|
|
189
|
+
"""This process's data home as a resolved path string, or '' when the
|
|
190
|
+
environment isn't readable (another user). Readable-but-unset
|
|
191
|
+
resolves to the default home, so it can be matched against a profile
|
|
192
|
+
home; a `--profile NAME` launch resolves to that profile's home by
|
|
193
|
+
the same rule `activate()` applies."""
|
|
194
|
+
env = _proc_environ(pid)
|
|
195
|
+
if env is None:
|
|
196
|
+
return ""
|
|
197
|
+
raw = env.get("PAINAPPLE_CODE_HOME", "")
|
|
198
|
+
base = Path(raw or "~/.painapple-code").expanduser() # unset → default
|
|
199
|
+
name = _profile_of(rest, env)
|
|
200
|
+
if name:
|
|
201
|
+
from painapple_code.cli.profiles import resolve_home
|
|
202
|
+
base = resolve_home(name, base)
|
|
203
|
+
return str(base.resolve())
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _saved_defaults(home, cache):
|
|
207
|
+
"""The saved serve values the process at data home `home` booted
|
|
208
|
+
with. '' (environment unreadable — another user) falls back to our
|
|
209
|
+
own view."""
|
|
210
|
+
if home not in cache:
|
|
211
|
+
from painapple_code.cli.serve_config import load
|
|
212
|
+
base = Path(home or os.environ.get("PAINAPPLE_CODE_HOME")
|
|
213
|
+
or "~/.painapple-code").expanduser()
|
|
214
|
+
# A PROFILE home keeps its serve values in profile.yaml — there is
|
|
215
|
+
# no serve.yaml beside it. Without this a profile instance reads
|
|
216
|
+
# back as built-in defaults (port 8765, cwd as workspace).
|
|
217
|
+
path = base / "profile.yaml"
|
|
218
|
+
if not path.is_file():
|
|
219
|
+
path = base / "serve.yaml"
|
|
220
|
+
cache[home] = load(path)[0]
|
|
221
|
+
return cache[home]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _workspace_of(pid, rest, saved):
|
|
225
|
+
ws = _flag(rest, "--workspace", "--cwd")
|
|
226
|
+
if ws and ws != ".":
|
|
227
|
+
return ws
|
|
228
|
+
# No flag → the saved default (serve.yaml) if any, else the
|
|
229
|
+
# process's cwd — readable on Linux for same-user processes.
|
|
230
|
+
if saved.get("workspace"):
|
|
231
|
+
return saved["workspace"]
|
|
232
|
+
import psutil
|
|
233
|
+
try:
|
|
234
|
+
return psutil.Process(pid).cwd()
|
|
235
|
+
except Exception:
|
|
236
|
+
return ws or ""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def local_servers():
|
|
240
|
+
"""[{pid, host, port, name, workspace}] for every running local
|
|
241
|
+
(non-container) painapple server process. Flag-less values fall back
|
|
242
|
+
to the saved serve defaults (each process's own serve.yaml), then
|
|
243
|
+
the built-ins — the same layering the server itself applies."""
|
|
244
|
+
me = os.getpid()
|
|
245
|
+
rows = []
|
|
246
|
+
cache = {}
|
|
247
|
+
for pid, argv in _iter_processes():
|
|
248
|
+
if pid == me:
|
|
249
|
+
continue
|
|
250
|
+
rest = _serve_args(argv)
|
|
251
|
+
if rest is None or _in_container(pid):
|
|
252
|
+
continue
|
|
253
|
+
home = _home_of(pid, rest)
|
|
254
|
+
saved = _saved_defaults(home, cache)
|
|
255
|
+
name = _flag(rest, "--instance-name",
|
|
256
|
+
default=saved.get("instance_name", ""))
|
|
257
|
+
if not name:
|
|
258
|
+
# Mirror the server's own defaulting: a profile instance
|
|
259
|
+
# without an explicit label uses its profile name.
|
|
260
|
+
from painapple_code.cli.serve_config import (
|
|
261
|
+
INSTANCE_NAME_MAX, PROFILE_PARENT_DIRS)
|
|
262
|
+
p = Path(home)
|
|
263
|
+
if p.parent.name in PROFILE_PARENT_DIRS:
|
|
264
|
+
name = p.name[:INSTANCE_NAME_MAX]
|
|
265
|
+
rows.append({
|
|
266
|
+
"pid": pid,
|
|
267
|
+
"host": _flag(rest, "--host", default=saved.get("host", "127.0.0.1")),
|
|
268
|
+
"port": _flag(rest, "--port", default=str(saved.get("port", 8765))),
|
|
269
|
+
"name": name,
|
|
270
|
+
"workspace": _workspace_of(pid, rest, saved),
|
|
271
|
+
"home": home,
|
|
272
|
+
# The exact token list — respawn and flag lookups read this.
|
|
273
|
+
# `command` is the joined form, for display only: never split
|
|
274
|
+
# it back apart (paths with spaces).
|
|
275
|
+
"argv": list(argv),
|
|
276
|
+
"command": " ".join(argv),
|
|
277
|
+
})
|
|
278
|
+
rows = _collapse_chains(rows)
|
|
279
|
+
rows.sort(key=lambda r: int(r["port"]) if r["port"].isdigit() else 0)
|
|
280
|
+
return rows
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _matched_ancestor(pid, by_pid):
|
|
284
|
+
"""Nearest ancestor of `pid` that is itself a matched server row and
|
|
285
|
+
belongs to the SAME server, or None. Never raises."""
|
|
286
|
+
try:
|
|
287
|
+
import psutil
|
|
288
|
+
seen = {pid}
|
|
289
|
+
cur = psutil.Process(pid)
|
|
290
|
+
while True:
|
|
291
|
+
ppid = cur.ppid()
|
|
292
|
+
if not ppid or ppid in seen:
|
|
293
|
+
return None
|
|
294
|
+
if ppid in by_pid:
|
|
295
|
+
return ppid
|
|
296
|
+
seen.add(ppid)
|
|
297
|
+
cur = psutil.Process(ppid)
|
|
298
|
+
except Exception:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _collapse_chains(rows):
|
|
303
|
+
"""One row per server, even when the server is a process chain.
|
|
304
|
+
|
|
305
|
+
A pipx console script on Windows is three processes — painapple.exe
|
|
306
|
+
launcher, the python.exe it starts, and uvicorn's reload child — all
|
|
307
|
+
carrying identical argv, so all three match and one server presented
|
|
308
|
+
as three rows. That put phantom entries in `painapple list`, and made
|
|
309
|
+
`stop` a coin flip: _kill terminates a single pid, so picking the
|
|
310
|
+
reload child left the supervisor to respawn it while picking the
|
|
311
|
+
launcher left the real server serving. Keep the topmost ancestor,
|
|
312
|
+
whose termination takes the rest of its own chain with it.
|
|
313
|
+
|
|
314
|
+
Only collapses within one server: the ancestor must agree on port and
|
|
315
|
+
data home. Running a second server from a terminal *inside* the first
|
|
316
|
+
is a genuinely separate deployment and keeps its own row.
|
|
317
|
+
"""
|
|
318
|
+
by_pid = {r["pid"]: r for r in rows}
|
|
319
|
+
parent = {}
|
|
320
|
+
for r in rows:
|
|
321
|
+
anc = _matched_ancestor(r["pid"], by_pid)
|
|
322
|
+
if anc is not None:
|
|
323
|
+
a = by_pid[anc]
|
|
324
|
+
if a["port"] == r["port"] and a.get("home") == r.get("home"):
|
|
325
|
+
parent[r["pid"]] = anc
|
|
326
|
+
|
|
327
|
+
def root_of(pid):
|
|
328
|
+
seen = set()
|
|
329
|
+
while pid in parent and pid not in seen:
|
|
330
|
+
seen.add(pid)
|
|
331
|
+
pid = parent[pid]
|
|
332
|
+
return pid
|
|
333
|
+
|
|
334
|
+
def depth(pid):
|
|
335
|
+
d, seen = 0, set()
|
|
336
|
+
while pid in parent and pid not in seen:
|
|
337
|
+
seen.add(pid)
|
|
338
|
+
pid = parent[pid]
|
|
339
|
+
d += 1
|
|
340
|
+
return d
|
|
341
|
+
|
|
342
|
+
kept = []
|
|
343
|
+
for r in rows:
|
|
344
|
+
if r["pid"] in parent:
|
|
345
|
+
continue
|
|
346
|
+
# The launcher shim's cwd is %TEMP%, not the project — so when the
|
|
347
|
+
# workspace fell back to cwd, take it from the DEEPEST process in
|
|
348
|
+
# the chain, which is the one actually serving. Explicit --workspace
|
|
349
|
+
# flags are identical across the chain (same argv), so this only
|
|
350
|
+
# ever changes the fallback case.
|
|
351
|
+
chain = [c for c in rows if root_of(c["pid"]) == r["pid"]]
|
|
352
|
+
if len(chain) > 1:
|
|
353
|
+
deepest = max(chain, key=lambda c: depth(c["pid"]))
|
|
354
|
+
if deepest["workspace"]:
|
|
355
|
+
r = {**r, "workspace": deepest["workspace"]}
|
|
356
|
+
kept.append(r)
|
|
357
|
+
return kept
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def match_process(running, home, port, exclude=()):
|
|
361
|
+
"""PID of the running server that belongs to the host deployment at
|
|
362
|
+
`home` listening on `port`, or None.
|
|
363
|
+
|
|
364
|
+
Order matters — a data home can legitimately hold more than one
|
|
365
|
+
process (someone started a second server out of it with --port), and
|
|
366
|
+
the deployment's OWN row is the one whose port also matches:
|
|
367
|
+
|
|
368
|
+
1. same data home AND same port — unambiguous
|
|
369
|
+
2. same data home — a port override of that deployment
|
|
370
|
+
3. UNKNOWN home, same port — macOS / other-user rows, where the
|
|
371
|
+
home can't be read at all. Restricted to unknown-home rows on
|
|
372
|
+
purpose: a row with a known, DIFFERENT home is a different
|
|
373
|
+
deployment that happens to sit on this port and must never be
|
|
374
|
+
claimed (or, in lifecycle_cmd, stopped) in its place.
|
|
375
|
+
"""
|
|
376
|
+
home_res = str(Path(home).expanduser().resolve())
|
|
377
|
+
port = str(port)
|
|
378
|
+
rows = [r for r in running if r["pid"] not in exclude]
|
|
379
|
+
for test in (lambda r: r.get("home") == home_res and r["port"] == port,
|
|
380
|
+
lambda r: r.get("home") == home_res,
|
|
381
|
+
lambda r: not r.get("home") and r["port"] == port):
|
|
382
|
+
pid = next((r["pid"] for r in rows if test(r)), None)
|
|
383
|
+
if pid is not None:
|
|
384
|
+
return pid
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _host_detail(pid, running, name):
|
|
389
|
+
"""The parenthetical for a matched host row: the pid, plus the
|
|
390
|
+
running process's own --instance-name when it differs from the row
|
|
391
|
+
name (the root deployment is very often labelled something else)."""
|
|
392
|
+
if not pid:
|
|
393
|
+
return ""
|
|
394
|
+
hit = next((r for r in running if r["pid"] == pid), {})
|
|
395
|
+
label = hit.get("name") or ""
|
|
396
|
+
return f"pid {pid} · {label}" if label and label != name else f"pid {pid}"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def root_row(running, exclude=()):
|
|
400
|
+
"""The flag-less root deployment as a deployment row — the target of
|
|
401
|
+
every verb called without a NAME. Always present: it's what a bare
|
|
402
|
+
`painapple` runs, whether or not it has ever been started."""
|
|
403
|
+
from painapple_code.cli.serve_config import load, root_home
|
|
404
|
+
home = root_home()
|
|
405
|
+
vals, _ = load(home / "serve.yaml")
|
|
406
|
+
port = str(vals.get("port", 8765))
|
|
407
|
+
pid = match_process(running, home, port, exclude)
|
|
408
|
+
hit = next((r for r in running if r["pid"] == pid), None) if pid else None
|
|
409
|
+
return {"name": "default", "mode": "host", "pid": pid,
|
|
410
|
+
"host": vals.get("host", "127.0.0.1"), "port": port,
|
|
411
|
+
"home": str(home),
|
|
412
|
+
# No saved workspace at the root — a bare `painapple` serves
|
|
413
|
+
# whatever directory it's launched from.
|
|
414
|
+
"workspace": (hit or {}).get("workspace") or "(cwd at launch)",
|
|
415
|
+
"detail": _host_detail(pid, running, "default")}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def deployment_rows(running):
|
|
419
|
+
"""Every named target, root deployment first: [{name, mode, pid,
|
|
420
|
+
host, port, workspace, detail}]. Profiles are matched before the
|
|
421
|
+
root row so a profile never loses its process to it."""
|
|
422
|
+
rows = profile_rows(running)
|
|
423
|
+
claimed = {r["pid"] for r in rows if r.get("pid")}
|
|
424
|
+
return [root_row(running, claimed), *rows]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def profile_rows(running):
|
|
428
|
+
"""[{name, mode, host, port, workspace, state}] for every saved
|
|
429
|
+
profile (profiles/NAME/profile.yaml). Host profiles match a running
|
|
430
|
+
local server by data home (exact, via psutil environ()) else by port;
|
|
431
|
+
docker profiles ask the container runtime."""
|
|
432
|
+
import shutil
|
|
433
|
+
from painapple_code.cli import profiles as profmod
|
|
434
|
+
|
|
435
|
+
probe = bool(shutil.which("docker") or shutil.which("podman"))
|
|
436
|
+
rows = []
|
|
437
|
+
for name in profmod.list_profiles():
|
|
438
|
+
prof = profmod.load(name)
|
|
439
|
+
if prof is None:
|
|
440
|
+
continue
|
|
441
|
+
if prof.is_docker:
|
|
442
|
+
cfg = prof.docker_settings()
|
|
443
|
+
status = None
|
|
444
|
+
if probe or cfg.runtime: # avoid auto_detect()'s die()
|
|
445
|
+
try:
|
|
446
|
+
from painapple_code.cli.deploy.runtime import Runtime
|
|
447
|
+
status = Runtime(cfg).container_status(cfg.container)
|
|
448
|
+
except (SystemExit, OSError):
|
|
449
|
+
status = None
|
|
450
|
+
ws = (f"{len(cfg.workspaces)} workspaces"
|
|
451
|
+
if cfg.workspace_mode == "multi"
|
|
452
|
+
else cfg.workspace or "(no workspace)")
|
|
453
|
+
rows.append({"name": name, "mode": "docker", "pid": None,
|
|
454
|
+
"host": cfg.listen_host, "port": str(cfg.port),
|
|
455
|
+
"home": "", "workspace": ws, "detail": status or ""})
|
|
456
|
+
continue
|
|
457
|
+
vals = prof.host_values()
|
|
458
|
+
port = str(vals.get("port", 8765))
|
|
459
|
+
home = prof.home()
|
|
460
|
+
pid = match_process(running, home, port)
|
|
461
|
+
rows.append({"name": name, "mode": "host", "pid": pid,
|
|
462
|
+
"host": vals.get("host", "127.0.0.1"), "port": port,
|
|
463
|
+
"home": str(home),
|
|
464
|
+
"workspace": vals.get("workspace")
|
|
465
|
+
or vals.get("instance_name") or "",
|
|
466
|
+
"detail": _host_detail(pid, running, name)})
|
|
467
|
+
return rows
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def adhoc_container_row():
|
|
471
|
+
"""The flag-less ``painapple --in-docker`` sandbox (container
|
|
472
|
+
'painapple-code'), when the runtime knows it. None otherwise."""
|
|
473
|
+
import shutil
|
|
474
|
+
if not (shutil.which("docker") or shutil.which("podman")):
|
|
475
|
+
return None
|
|
476
|
+
try:
|
|
477
|
+
from painapple_code.cli.deploy.config import DockerSettings
|
|
478
|
+
from painapple_code.cli.deploy.runtime import Runtime
|
|
479
|
+
cfg = DockerSettings(profile=None)
|
|
480
|
+
rt = Runtime(cfg)
|
|
481
|
+
if not rt.container_exists(cfg.container):
|
|
482
|
+
return None
|
|
483
|
+
status = rt.container_status(cfg.container)
|
|
484
|
+
return {"name": cfg.container, "host": cfg.listen_host,
|
|
485
|
+
"port": str(cfg.port), "detail": status or ""}
|
|
486
|
+
except (SystemExit, OSError):
|
|
487
|
+
return None
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# ──── Command ─────────────────────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
def _state(detail):
|
|
493
|
+
return (f"{GREEN}running{RESET} {DIM}({detail}){RESET}"
|
|
494
|
+
if detail else f"{DIM}stopped{RESET}")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def main(argv=None):
|
|
498
|
+
from painapple_code.cli import profiles as profmod
|
|
499
|
+
profmod.ensure_migrated()
|
|
500
|
+
|
|
501
|
+
say(f"{BOLD}Painapple Code instances{RESET}")
|
|
502
|
+
say()
|
|
503
|
+
|
|
504
|
+
locals_ = local_servers()
|
|
505
|
+
rows = deployment_rows(locals_)
|
|
506
|
+
claimed = {r["pid"] for r in rows if r.get("pid")}
|
|
507
|
+
# Servers in the process table that belong to no deployment: started
|
|
508
|
+
# directly (by hand, a service unit) with their own flags, so their
|
|
509
|
+
# name is just their --instance-name and no NAME verb targets them.
|
|
510
|
+
unmanaged = [r for r in locals_ if r["pid"] not in claimed]
|
|
511
|
+
saved = [r for r in rows if r["name"] != "default"]
|
|
512
|
+
|
|
513
|
+
say(f"{BOLD}Deployments{RESET} {DIM}(named targets — "
|
|
514
|
+
f"painapple start/stop/status NAME){RESET}")
|
|
515
|
+
width = max(len(r["name"]) for r in rows)
|
|
516
|
+
mwidth = max(len(r["mode"]) for r in rows) + 2 # brackets
|
|
517
|
+
for r in rows:
|
|
518
|
+
badge = f"[{r['mode']}]"
|
|
519
|
+
say((f" {BOLD}{r['name']:<{width}}{RESET} "
|
|
520
|
+
f"{DIM}{badge}{RESET}{'':<{mwidth - len(badge)}}"
|
|
521
|
+
f" {r['host']}:{r['port']} {_state(r['detail'])}"
|
|
522
|
+
f" {DIM}{r['workspace']}{RESET}").rstrip())
|
|
523
|
+
if not saved:
|
|
524
|
+
say(f" {DIM}default = what a bare `painapple` runs; add named ones "
|
|
525
|
+
f"with: painapple setup NAME{RESET}")
|
|
526
|
+
|
|
527
|
+
if unmanaged:
|
|
528
|
+
say()
|
|
529
|
+
say(f"{BOLD}Unmanaged processes{RESET} {DIM}(started directly — "
|
|
530
|
+
f"not from a saved deployment){RESET}")
|
|
531
|
+
width = max(len(r["name"] or "—") for r in unmanaged)
|
|
532
|
+
for r in unmanaged:
|
|
533
|
+
say((f" {BOLD}{r['name'] or '—':<{width}}{RESET}"
|
|
534
|
+
f" {r['host']}:{r['port']}"
|
|
535
|
+
f" {GREEN}running{RESET} {DIM}(pid {r['pid']}){RESET}"
|
|
536
|
+
f" {DIM}{r['workspace']}{RESET}").rstrip())
|
|
537
|
+
say(f" {DIM}their name is just the --instance-name label; "
|
|
538
|
+
f"stop/restart/status take it too{RESET}")
|
|
539
|
+
|
|
540
|
+
adhoc = adhoc_container_row()
|
|
541
|
+
if adhoc:
|
|
542
|
+
say()
|
|
543
|
+
say(f"{BOLD}Ad-hoc container{RESET} {DIM}(painapple --in-docker){RESET}")
|
|
544
|
+
say(f" {BOLD}{adhoc['name']}{RESET} {adhoc['host']}:{adhoc['port']}"
|
|
545
|
+
f" {_state(adhoc['detail'])}")
|
|
546
|
+
|
|
547
|
+
say()
|
|
548
|
+
say(f"{BOLD}Address{RESET} {DIM}= the host it binds to{RESET}")
|
|
549
|
+
say(f" {'0.0.0.0':<9} {DIM}all interfaces — reachable from the LAN{RESET}")
|
|
550
|
+
say(f" {'127.0.0.1':<9} {DIM}localhost only{RESET}")
|
|
551
|
+
|
|
552
|
+
say()
|
|
553
|
+
if saved:
|
|
554
|
+
# Past first run: one pointer, not a seven-line cheat sheet.
|
|
555
|
+
say(f"{DIM}Details for one:{RESET} painapple status NAME "
|
|
556
|
+
f"{DIM}· full help:{RESET} painapple help")
|
|
557
|
+
else:
|
|
558
|
+
say(f"{BOLD}Commands{RESET} {DIM}(NAME = a deployment above; omit it "
|
|
559
|
+
f"to target `default`){RESET}")
|
|
560
|
+
hints = [
|
|
561
|
+
("painapple setup NAME", "create or reconfigure a profile"),
|
|
562
|
+
("painapple start NAME", "run it in the background"),
|
|
563
|
+
("painapple stop NAME", "stop it"),
|
|
564
|
+
("painapple restart NAME", "stop, then start again"),
|
|
565
|
+
("painapple status NAME", "running or not, address, workspace"),
|
|
566
|
+
("painapple logs NAME", "tail its log"),
|
|
567
|
+
("painapple password NAME", "print its login URL"),
|
|
568
|
+
]
|
|
569
|
+
width = max(len(cmd) for cmd, _ in hints)
|
|
570
|
+
for cmd, desc in hints:
|
|
571
|
+
say(f" {cmd:<{width}} {DIM}{desc}{RESET}")
|
|
572
|
+
return 0
|