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,443 @@
|
|
|
1
|
+
"""``painapple start/stop/restart`` — background lifecycle for local
|
|
2
|
+
host-mode instances (the action verbs next to ``painapple list``).
|
|
3
|
+
|
|
4
|
+
There is no daemon or PID registry — a painapple server process IS the
|
|
5
|
+
instance — so these commands reuse ``list_cmd``'s process-table
|
|
6
|
+
discovery to find the target, and plain signals to stop it:
|
|
7
|
+
|
|
8
|
+
painapple start [NAME] spawn detached (logs to <home>/logs/),
|
|
9
|
+
wait until the port accepts, print the
|
|
10
|
+
login URL
|
|
11
|
+
painapple stop [NAME] SIGTERM → poll → SIGKILL
|
|
12
|
+
painapple restart [NAME] stop (if running), then start
|
|
13
|
+
|
|
14
|
+
``NAME`` resolves in order: a saved serve profile (``painapple setup
|
|
15
|
+
NAME``) → a running instance's label (exact, then case-insensitive) →
|
|
16
|
+
its PID → its port — so anything `painapple list` prints is a valid
|
|
17
|
+
target. Omitted (or ``default``) targets the flag-less root deployment.
|
|
18
|
+
|
|
19
|
+
Profile targets restart from their saved serve.yaml. A label/pid/port
|
|
20
|
+
target has no saved config — it was launched ad hoc with flags — so
|
|
21
|
+
``restart`` recaptures the live process's own command line, working
|
|
22
|
+
directory, and environment (via psutil, so on all three platforms) and
|
|
23
|
+
respawns it verbatim.
|
|
24
|
+
|
|
25
|
+
Extra serve flags after the name are forwarded to the spawned server
|
|
26
|
+
(``painapple start work --port 9001``; on an ad-hoc restart they're
|
|
27
|
+
appended, and argparse last-wins makes them override) — they apply to
|
|
28
|
+
THIS start only, they are not saved.
|
|
29
|
+
|
|
30
|
+
Matching a profile to a running process mirrors ``list_cmd.serve_profiles``:
|
|
31
|
+
exact data-home match (psutil ``environ()``) first, port match as the
|
|
32
|
+
fallback for processes whose environment isn't readable.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
import re
|
|
37
|
+
import socket
|
|
38
|
+
import subprocess
|
|
39
|
+
import sys
|
|
40
|
+
import time
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
from painapple_code.cli import profiles, serve_config
|
|
44
|
+
from painapple_code.cli.ui import BOLD, DIM, GREEN, RESET, err, info, ok, say, warn
|
|
45
|
+
from painapple_code.utils.proc import pid_alive
|
|
46
|
+
|
|
47
|
+
START_TIMEOUT = 20 # seconds for the port to start accepting
|
|
48
|
+
STOP_TIMEOUT = 10 # seconds after SIGTERM before escalating to SIGKILL
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ──── Target resolution ──────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
def _resolve(prof):
|
|
54
|
+
"""{profile, label, home, host, port} for a SAVED HOST target: a
|
|
55
|
+
profile name that exists on disk, or None = the root deployment."""
|
|
56
|
+
if prof:
|
|
57
|
+
home = profiles.profile_home(prof)
|
|
58
|
+
loaded = profiles.load(prof)
|
|
59
|
+
vals = loaded.host_values() if loaded else {}
|
|
60
|
+
else:
|
|
61
|
+
home = serve_config.home_root()
|
|
62
|
+
vals, _ = serve_config.load(home / "serve.yaml")
|
|
63
|
+
return {
|
|
64
|
+
"profile": prof,
|
|
65
|
+
"label": prof or "default",
|
|
66
|
+
"home": home,
|
|
67
|
+
"host": vals.get("host", "127.0.0.1"),
|
|
68
|
+
"port": int(vals.get("port", 8765)),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _match(running, target):
|
|
73
|
+
"""PID of the running server for this saved target, or None — the
|
|
74
|
+
same home/port matching `painapple list` uses to fill its rows, so
|
|
75
|
+
what the fleet view calls running is exactly what stop targets."""
|
|
76
|
+
from painapple_code.cli.list_cmd import match_process
|
|
77
|
+
return match_process(running, target["home"], target["port"])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _match_running(token):
|
|
81
|
+
"""(row, problem) — the running local server `token` names, by
|
|
82
|
+
instance label (exact, then case-insensitive), PID, or port.
|
|
83
|
+
(None, None) when nothing matches; (None, message) when the token is
|
|
84
|
+
ambiguous."""
|
|
85
|
+
from painapple_code.cli.list_cmd import local_servers
|
|
86
|
+
rows = local_servers()
|
|
87
|
+
matches = [r for r in rows if r["name"] and r["name"] == token]
|
|
88
|
+
if not matches:
|
|
89
|
+
matches = [r for r in rows
|
|
90
|
+
if r["name"] and r["name"].lower() == token.lower()]
|
|
91
|
+
if not matches and token.isdigit():
|
|
92
|
+
matches = ([r for r in rows if str(r["pid"]) == token]
|
|
93
|
+
or [r for r in rows if r["port"] == token])
|
|
94
|
+
if len(matches) > 1:
|
|
95
|
+
listing = ", ".join(f"{r['name'] or '—'} (pid {r['pid']}, "
|
|
96
|
+
f"port {r['port']})" for r in matches)
|
|
97
|
+
return None, f"{token!r} is ambiguous — matches {listing}. Use the pid."
|
|
98
|
+
return (matches[0] if matches else None), None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _row_label(row):
|
|
102
|
+
return row["name"] or f"pid {row['pid']}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ──── stop ───────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def _wait_gone(pid, timeout):
|
|
108
|
+
# pid_alive is psutil, not os.kill(pid, 0): the POSIX idiom lies on
|
|
109
|
+
# Windows (sig 0 is CTRL_C_EVENT and succeeds even for dead pids).
|
|
110
|
+
# Same contract: exists → True, including other users' processes.
|
|
111
|
+
deadline = time.monotonic() + timeout
|
|
112
|
+
while time.monotonic() < deadline:
|
|
113
|
+
if not pid_alive(pid):
|
|
114
|
+
return True
|
|
115
|
+
time.sleep(0.2)
|
|
116
|
+
return not pid_alive(pid)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _kill(label, pid):
|
|
120
|
+
# psutil, not raw signals: SIGKILL doesn't exist on win32 and os.kill
|
|
121
|
+
# semantics differ there; terminate() is SIGTERM on POSIX and
|
|
122
|
+
# TerminateProcess on Windows.
|
|
123
|
+
import psutil
|
|
124
|
+
from painapple_code.utils.proc import kill_pid
|
|
125
|
+
try:
|
|
126
|
+
psutil.Process(pid).terminate()
|
|
127
|
+
except psutil.NoSuchProcess:
|
|
128
|
+
say(f"{DIM}{label}: not running{RESET}")
|
|
129
|
+
return 0
|
|
130
|
+
except psutil.AccessDenied:
|
|
131
|
+
err(f"{label}: pid {pid} belongs to another user — can't stop it")
|
|
132
|
+
return 1
|
|
133
|
+
if not _wait_gone(pid, STOP_TIMEOUT):
|
|
134
|
+
warn(f"{label}: pid {pid} ignored SIGTERM for "
|
|
135
|
+
f"{STOP_TIMEOUT}s — sending SIGKILL")
|
|
136
|
+
kill_pid(pid)
|
|
137
|
+
if not _wait_gone(pid, 3):
|
|
138
|
+
err(f"{label}: pid {pid} would not die")
|
|
139
|
+
return 1
|
|
140
|
+
ok(f"Stopped {label} (pid {pid})")
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _stop(target):
|
|
145
|
+
from painapple_code.cli.list_cmd import local_servers
|
|
146
|
+
pid = _match(local_servers(), target)
|
|
147
|
+
if pid is None:
|
|
148
|
+
say(f"{DIM}{target['label']}: not running{RESET}")
|
|
149
|
+
return 0
|
|
150
|
+
return _kill(target["label"], pid)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ──── start ──────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
def _spawn_env(target):
|
|
156
|
+
"""Child environment: the profile's data home exported explicitly
|
|
157
|
+
(so `painapple list` can home-match it via /proc/<pid>/environ) and
|
|
158
|
+
PAINAPPLE_PROFILE stamped for the instance-name default. The child's
|
|
159
|
+
activate() recognizes an already-profile-shaped home and doesn't
|
|
160
|
+
nest it (see profiles.activate)."""
|
|
161
|
+
env = {k: v for k, v in os.environ.items() if k != "PAINAPPLE_PROFILE"}
|
|
162
|
+
if target["profile"]:
|
|
163
|
+
env["PAINAPPLE_CODE_HOME"] = str(target["home"])
|
|
164
|
+
env["PAINAPPLE_PROFILE"] = target["profile"]
|
|
165
|
+
return env
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _print_login_url(*logs):
|
|
169
|
+
"""Echo the login URL so a background start is as usable as a
|
|
170
|
+
foreground one.
|
|
171
|
+
|
|
172
|
+
Scans each log in turn: the URL lives in the startup box on stdout
|
|
173
|
+
(console.log), with server.log kept as a fallback for older builds
|
|
174
|
+
that logged it as an INFO line instead. Non-loopback binds hide the
|
|
175
|
+
?tkn= URL from their own stdout by default, so when only the bare
|
|
176
|
+
box URL is found we rebuild the full login URL from the auth config
|
|
177
|
+
— `painapple start` runs as the operator on this box, who can read
|
|
178
|
+
that file anyway (it's exactly what `painapple password` prints)."""
|
|
179
|
+
bare = None
|
|
180
|
+
for log in logs:
|
|
181
|
+
try:
|
|
182
|
+
text = log.read_text(encoding="utf-8", errors="replace")[-8000:]
|
|
183
|
+
except OSError:
|
|
184
|
+
continue
|
|
185
|
+
for line in reversed(text.splitlines()):
|
|
186
|
+
m = re.search(r"https?://\S+\?tkn=\S+", line)
|
|
187
|
+
if m:
|
|
188
|
+
say(f" {BOLD}Log in:{RESET} {BOLD}{GREEN}{m.group(0)}{RESET}")
|
|
189
|
+
return
|
|
190
|
+
if bare is None and "pAInapple Code:" in line:
|
|
191
|
+
b = re.search(r"https?://\S+", line)
|
|
192
|
+
if b:
|
|
193
|
+
bare = b.group(0).rstrip("/")
|
|
194
|
+
if bare:
|
|
195
|
+
from painapple_code.cli.manage_cmd import _host_token_value
|
|
196
|
+
token = _host_token_value()
|
|
197
|
+
url = f"{bare}/?tkn={token}" if token else f"{bare}/"
|
|
198
|
+
say(f" {BOLD}Log in:{RESET} {BOLD}{GREEN}{url}{RESET}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _tail(path, n=15):
|
|
202
|
+
try:
|
|
203
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()[-n:]
|
|
204
|
+
except OSError:
|
|
205
|
+
return
|
|
206
|
+
for line in lines:
|
|
207
|
+
say(f" {DIM}{line}{RESET}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _spawn_and_wait(label, argv, *, env, cwd, home, host, port):
|
|
211
|
+
"""Detached spawn + port-readiness wait + login-URL echo — shared by
|
|
212
|
+
profile starts and ad-hoc restarts. env/cwd None = inherit ours."""
|
|
213
|
+
logs = Path(home).expanduser() / "logs"
|
|
214
|
+
logs.mkdir(parents=True, exist_ok=True)
|
|
215
|
+
console = logs / "console.log"
|
|
216
|
+
with open(console, "ab") as out:
|
|
217
|
+
out.write(f"\n--- painapple start {label} ---\n".encode())
|
|
218
|
+
out.flush()
|
|
219
|
+
from painapple_code.utils.proc import popen_kwargs_detached
|
|
220
|
+
proc = subprocess.Popen(
|
|
221
|
+
argv, stdout=out, stderr=subprocess.STDOUT,
|
|
222
|
+
stdin=subprocess.DEVNULL, env=env, cwd=cwd,
|
|
223
|
+
**popen_kwargs_detached(fully_detached=True))
|
|
224
|
+
|
|
225
|
+
info(f"Starting {label} (pid {proc.pid}) on {host}:{port} …")
|
|
226
|
+
probe_host = "127.0.0.1" if host in ("0.0.0.0", "::") else host
|
|
227
|
+
deadline = time.monotonic() + START_TIMEOUT
|
|
228
|
+
while time.monotonic() < deadline:
|
|
229
|
+
if proc.poll() is not None:
|
|
230
|
+
err(f"{label} exited during startup "
|
|
231
|
+
f"(code {proc.returncode}) — last output:")
|
|
232
|
+
_tail(console)
|
|
233
|
+
return 1
|
|
234
|
+
try:
|
|
235
|
+
with socket.create_connection((probe_host, port), timeout=1):
|
|
236
|
+
pass
|
|
237
|
+
except OSError:
|
|
238
|
+
time.sleep(0.3)
|
|
239
|
+
continue
|
|
240
|
+
# Something answers — but make sure it's OUR child and not a
|
|
241
|
+
# foreign listener that was already there (ours would have died
|
|
242
|
+
# on its own bind pre-flight, a moment after this connect).
|
|
243
|
+
time.sleep(0.3)
|
|
244
|
+
if proc.poll() is None:
|
|
245
|
+
break
|
|
246
|
+
err(f"{label} exited during startup (code {proc.returncode}) — "
|
|
247
|
+
f"something else is answering on {probe_host}:{port}. "
|
|
248
|
+
f"Last output:")
|
|
249
|
+
_tail(console)
|
|
250
|
+
return 1
|
|
251
|
+
else:
|
|
252
|
+
warn(f"{label}: pid {proc.pid} is up but {probe_host}:{port} "
|
|
253
|
+
f"didn't accept within {START_TIMEOUT}s — check {console}")
|
|
254
|
+
return 1
|
|
255
|
+
|
|
256
|
+
ok(f"{label} running — pid {proc.pid} on {host}:{port}")
|
|
257
|
+
_print_login_url(console, logs / "server.log")
|
|
258
|
+
return 0
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _start(target, extra):
|
|
262
|
+
from painapple_code.cli.list_cmd import local_servers
|
|
263
|
+
running = local_servers()
|
|
264
|
+
pid = _match(running, target)
|
|
265
|
+
if pid is not None:
|
|
266
|
+
warn(f"{target['label']} is already running (pid {pid}) — "
|
|
267
|
+
f"use: painapple restart {target['profile'] or ''}".rstrip())
|
|
268
|
+
return 1
|
|
269
|
+
|
|
270
|
+
# Validate forwarded serve flags HERE (same import-light parser the
|
|
271
|
+
# serve gate uses) so a typo fails loudly instead of inside a
|
|
272
|
+
# detached child. parse_args exits 2 with usage on a bad flag.
|
|
273
|
+
if extra:
|
|
274
|
+
from painapple_code.cli.serve_args import build_parser
|
|
275
|
+
build_parser().parse_args(extra)
|
|
276
|
+
|
|
277
|
+
host = _cli_flag(extra, "--host") or target["host"]
|
|
278
|
+
port = int(_cli_flag(extra, "--port") or target["port"])
|
|
279
|
+
|
|
280
|
+
# Bind test, not a scan of our own process table — a port held by
|
|
281
|
+
# ANYTHING (another painapple, a stray dev server) must fail here,
|
|
282
|
+
# loudly, rather than inside a detached child nobody is watching.
|
|
283
|
+
from painapple_code.cli.netinfo import port_holder, port_taken
|
|
284
|
+
reason = port_taken(host, port)
|
|
285
|
+
if reason:
|
|
286
|
+
holder = port_holder(port)
|
|
287
|
+
err(f"Port {port} on {host} is already in use — {reason}"
|
|
288
|
+
+ (f" (held by {holder})" if holder else ""))
|
|
289
|
+
return 1
|
|
290
|
+
|
|
291
|
+
return _spawn_and_wait(
|
|
292
|
+
target["label"], [sys.executable, "-m", "painapple_code", *extra],
|
|
293
|
+
env=_spawn_env(target), cwd=None, home=target["home"],
|
|
294
|
+
host=host, port=port)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _cli_flag(args, name):
|
|
298
|
+
for i, tok in enumerate(args):
|
|
299
|
+
if tok == name and i + 1 < len(args):
|
|
300
|
+
return args[i + 1]
|
|
301
|
+
if tok.startswith(name + "="):
|
|
302
|
+
return tok.split("=", 1)[1]
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ──── Ad-hoc targets (label / pid / port of a flag-launched server) ──────
|
|
307
|
+
|
|
308
|
+
def _respawn_spec(row):
|
|
309
|
+
"""(argv, cwd, env) to faithfully relaunch this running server, or None.
|
|
310
|
+
|
|
311
|
+
psutil for all three fields — the same wrappers `painapple list`
|
|
312
|
+
already uses. The hand-rolled /proc cmdline/cwd/environ reader this
|
|
313
|
+
replaces was the PREFERRED branch and Linux-only, so the platforms
|
|
314
|
+
this branch adds fell through to `return list(argv), None, None` and
|
|
315
|
+
respawned WITHOUT the process's working directory or environment:
|
|
316
|
+
lossy on exactly the hosts nobody exercises locally.
|
|
317
|
+
|
|
318
|
+
Each field is best-effort on its own — AccessDenied reading environ()
|
|
319
|
+
must not cost us the argv — and the process-scan argv stays as the
|
|
320
|
+
fallback for cmdline().
|
|
321
|
+
"""
|
|
322
|
+
import psutil
|
|
323
|
+
|
|
324
|
+
from painapple_code.cli.list_cmd import _proc_environ
|
|
325
|
+
|
|
326
|
+
pid = row["pid"]
|
|
327
|
+
argv = list(row.get("argv") or [])
|
|
328
|
+
cwd = None
|
|
329
|
+
env = _proc_environ(pid) # None = unreadable → inherit ours
|
|
330
|
+
try:
|
|
331
|
+
proc = psutil.Process(pid)
|
|
332
|
+
argv = list(proc.cmdline()) or argv
|
|
333
|
+
cwd = proc.cwd()
|
|
334
|
+
except Exception:
|
|
335
|
+
pass # other-user/vanished process — the kill will refuse anyway
|
|
336
|
+
return (argv, cwd, env) if argv else None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _restart_adhoc(row, extra):
|
|
340
|
+
"""Restart a running instance that has NO saved profile: recapture
|
|
341
|
+
its command line BEFORE killing it, then respawn verbatim (+ any
|
|
342
|
+
forwarded flags, which argparse last-wins into overrides)."""
|
|
343
|
+
label = _row_label(row)
|
|
344
|
+
spec = _respawn_spec(row)
|
|
345
|
+
if spec is None:
|
|
346
|
+
err(f"{label}: can't recover the process command line to respawn it")
|
|
347
|
+
return 1
|
|
348
|
+
if extra:
|
|
349
|
+
from painapple_code.cli.serve_args import build_parser
|
|
350
|
+
build_parser().parse_args(extra)
|
|
351
|
+
argv, cwd, env = spec
|
|
352
|
+
rc = _kill(label, row["pid"])
|
|
353
|
+
if rc:
|
|
354
|
+
return rc
|
|
355
|
+
host = _cli_flag(extra, "--host") or row["host"]
|
|
356
|
+
port = int(_cli_flag(extra, "--port") or row["port"])
|
|
357
|
+
home = row.get("home") or os.environ.get("PAINAPPLE_CODE_HOME",
|
|
358
|
+
"~/.painapple-code")
|
|
359
|
+
return _spawn_and_wait(label, argv + list(extra), env=env, cwd=cwd,
|
|
360
|
+
home=home, host=host, port=port)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# ──── Command ────────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
def main(cmd, argv):
|
|
366
|
+
"""Dispatch target for start/stop/restart. The target is a leading
|
|
367
|
+
positional (``painapple restart work`` / ``STAGING`` / a pid / a
|
|
368
|
+
port), ``--profile NAME``, or $PAINAPPLE_PROFILE; nothing targets
|
|
369
|
+
the root deployment."""
|
|
370
|
+
try:
|
|
371
|
+
prof, rest = serve_config.extract_profile(list(argv or []))
|
|
372
|
+
except ValueError as e:
|
|
373
|
+
err(str(e))
|
|
374
|
+
return 2
|
|
375
|
+
if rest and not rest[0].startswith("-"):
|
|
376
|
+
if prof and prof != rest[0]:
|
|
377
|
+
err(f"Profile given twice: {rest[0]!r} and --profile {prof!r}")
|
|
378
|
+
return 2
|
|
379
|
+
prof, rest = rest[0], rest[1:]
|
|
380
|
+
if prof in ("", "default"):
|
|
381
|
+
prof = None
|
|
382
|
+
if cmd == "stop" and rest:
|
|
383
|
+
err(f"stop takes no flags (got: {' '.join(rest)})")
|
|
384
|
+
return 2
|
|
385
|
+
|
|
386
|
+
profiles.ensure_migrated()
|
|
387
|
+
|
|
388
|
+
# A saved DOCKER profile: the container runtime already is the
|
|
389
|
+
# background lifecycle — start = run -d, stop = container stop.
|
|
390
|
+
if prof and profiles.exists(prof):
|
|
391
|
+
loaded = profiles.load(prof)
|
|
392
|
+
if loaded is not None and loaded.is_docker:
|
|
393
|
+
return _docker_lifecycle(cmd, loaded, rest)
|
|
394
|
+
|
|
395
|
+
# Not a saved profile → try what `painapple list` shows: a running
|
|
396
|
+
# instance's label, pid, or port.
|
|
397
|
+
if prof and not profiles.exists(prof):
|
|
398
|
+
row, problem = _match_running(prof)
|
|
399
|
+
if problem:
|
|
400
|
+
err(problem)
|
|
401
|
+
return 1
|
|
402
|
+
if row is not None:
|
|
403
|
+
if cmd == "stop":
|
|
404
|
+
return _kill(_row_label(row), row["pid"])
|
|
405
|
+
if cmd == "restart":
|
|
406
|
+
return _restart_adhoc(row, rest)
|
|
407
|
+
warn(f"{_row_label(row)} is already running (pid {row['pid']}) — "
|
|
408
|
+
f"use: painapple restart {prof}")
|
|
409
|
+
return 1
|
|
410
|
+
err(f"{prof!r} matches no saved profile and no running instance "
|
|
411
|
+
f"(label, pid, or port).")
|
|
412
|
+
if profiles.valid_name(prof):
|
|
413
|
+
say(f" {DIM}Create a profile with:{RESET} painapple setup {prof}")
|
|
414
|
+
return 1
|
|
415
|
+
|
|
416
|
+
target = _resolve(prof)
|
|
417
|
+
if cmd == "stop":
|
|
418
|
+
return _stop(target)
|
|
419
|
+
if cmd == "restart":
|
|
420
|
+
rc = _stop(target)
|
|
421
|
+
if rc:
|
|
422
|
+
return rc
|
|
423
|
+
return _start(target, rest)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _docker_lifecycle(cmd, loaded, rest):
|
|
427
|
+
"""start/stop/restart for a docker-mode profile — delegates to the
|
|
428
|
+
container runtime (detached runs are durable: --restart
|
|
429
|
+
unless-stopped)."""
|
|
430
|
+
from painapple_code.cli.deploy.container import (
|
|
431
|
+
remove_container, stop_container)
|
|
432
|
+
from painapple_code.cli.deploy.launch import profile_settings
|
|
433
|
+
try:
|
|
434
|
+
cfg = profile_settings(loaded, rest)
|
|
435
|
+
if cmd == "stop":
|
|
436
|
+
return stop_container(cfg, profile=loaded.name)
|
|
437
|
+
if cmd == "restart":
|
|
438
|
+
remove_container(cfg)
|
|
439
|
+
from painapple_code.cli.deploy.container import run_container
|
|
440
|
+
return run_container(cfg, detach=True, profile=loaded.name)
|
|
441
|
+
except KeyboardInterrupt:
|
|
442
|
+
say()
|
|
443
|
+
return 130
|