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,163 @@
|
|
|
1
|
+
"""Launch assembly for ``painapple --in-docker`` and docker-mode
|
|
2
|
+
profiles — turns global defaults / profile data / explicit serve flags
|
|
3
|
+
into DockerSettings and hands off to container.run_container().
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from painapple_code.cli import serve_config
|
|
9
|
+
from painapple_code.cli.ui import DIM, RESET, info, warn
|
|
10
|
+
from painapple_code.cli.deploy.config import ConfigError, DockerSettings
|
|
11
|
+
from painapple_code.cli.deploy.container import run_container
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _flag(argv, *names):
|
|
15
|
+
"""Value of an explicitly passed ``--flag value`` / ``--flag=value``,
|
|
16
|
+
or None. Used to tell an explicit flag from an argparse default."""
|
|
17
|
+
for i, tok in enumerate(argv):
|
|
18
|
+
for name in names:
|
|
19
|
+
if tok == name and i + 1 < len(argv):
|
|
20
|
+
return argv[i + 1]
|
|
21
|
+
if tok.startswith(name + "="):
|
|
22
|
+
return tok.split("=", 1)[1]
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _try_assign(cfg, key, value, source):
|
|
27
|
+
try:
|
|
28
|
+
cfg.assign(key, str(value))
|
|
29
|
+
except ConfigError as e:
|
|
30
|
+
warn(f"{source}: {e} — keeping {cfg.get(key)!r}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _apply_serve_flags(cfg, argv):
|
|
34
|
+
"""Explicit serve flags override whatever the settings came from —
|
|
35
|
+
same precedence story as host mode (defaults < saved < flags)."""
|
|
36
|
+
overrides = (("--host", "LISTEN_HOST"), ("--port", "PORT"),
|
|
37
|
+
("--tls", "TLS_MODE"), ("--instance-name", "INSTANCE_NAME"),
|
|
38
|
+
("--accent", "ACCENT"))
|
|
39
|
+
for flag, key in overrides:
|
|
40
|
+
value = _flag(argv, flag)
|
|
41
|
+
if value is not None:
|
|
42
|
+
_try_assign(cfg, key, value, flag)
|
|
43
|
+
# The container invocation is assembled from DockerSettings, not the
|
|
44
|
+
# host argv, so the password-visibility flags don't forward. That's
|
|
45
|
+
# fine for --no-password: the container binds non-loopback internally,
|
|
46
|
+
# so its startup box hides credentials by default anyway. --show-password
|
|
47
|
+
# would need forwarding to have any effect — warn instead of silently
|
|
48
|
+
# dropping it (the host-side bootstrap block prints the URL regardless).
|
|
49
|
+
if "--show-password" in argv:
|
|
50
|
+
warn("--show-password is not forwarded into the container; its "
|
|
51
|
+
"startup box hides credentials (non-loopback bind). The "
|
|
52
|
+
"login URL is printed host-side after start, or via "
|
|
53
|
+
"`painapple password`.")
|
|
54
|
+
ws = _flag(argv, "--workspace", "--cwd")
|
|
55
|
+
if ws is not None:
|
|
56
|
+
_try_assign(cfg, "WORKSPACE", ws, "--workspace")
|
|
57
|
+
return True
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _auto_workspace_mode(cfg):
|
|
62
|
+
"""Zero-config mode pick: a git checkout → 'project', anything else
|
|
63
|
+
→ 'parent' — matching "I'm in a repo" vs "I opened a folder of
|
|
64
|
+
repos" without asking. (.git can be a dir or a worktree file.)"""
|
|
65
|
+
ws = Path(cfg.workspace or ".")
|
|
66
|
+
mode = "project" if (ws / ".git").exists() else "parent"
|
|
67
|
+
reason = "found .git" if mode == "project" else "no .git"
|
|
68
|
+
cfg.workspace_mode = mode
|
|
69
|
+
info(f"{ws} → {mode} mode {DIM}({reason}){RESET}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def docker_settings_with_globals(prof):
|
|
73
|
+
"""A docker-mode profile's settings with the GLOBAL runtime/image
|
|
74
|
+
defaults filled in wherever the profile doesn't pin its own (the
|
|
75
|
+
wizard leaves them unset by default)."""
|
|
76
|
+
cfg = prof.docker_settings()
|
|
77
|
+
gvalues, _ = serve_config.load(serve_config.root_home() / "serve.yaml")
|
|
78
|
+
for key, cfg_key in (("runtime", "RUNTIME"),
|
|
79
|
+
("runtime_flags", "RUNTIME_FLAGS"),
|
|
80
|
+
("image", "IMAGE")):
|
|
81
|
+
if not prof.data.get(key) and gvalues.get(key) not in (None, ""):
|
|
82
|
+
_try_assign(cfg, cfg_key, gvalues[key], "serve.yaml")
|
|
83
|
+
return cfg
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def adhoc_settings(argv):
|
|
87
|
+
"""DockerSettings for a bare ``painapple --in-docker``: global
|
|
88
|
+
serve.yaml defaults (network + runtime/image) + explicit serve
|
|
89
|
+
flags, cwd as the workspace."""
|
|
90
|
+
cfg = DockerSettings(profile=None)
|
|
91
|
+
values, problems = serve_config.load()
|
|
92
|
+
for problem in problems:
|
|
93
|
+
warn(f"serve.yaml: {problem}")
|
|
94
|
+
for key, cfg_key in (("runtime", "RUNTIME"), ("runtime_flags", "RUNTIME_FLAGS"),
|
|
95
|
+
("image", "IMAGE"), ("host", "LISTEN_HOST"),
|
|
96
|
+
("port", "PORT"), ("tls", "TLS_MODE")):
|
|
97
|
+
if values.get(key) not in (None, ""):
|
|
98
|
+
_try_assign(cfg, cfg_key, values[key], "serve.yaml")
|
|
99
|
+
|
|
100
|
+
explicit_ws = _apply_serve_flags(cfg, argv)
|
|
101
|
+
if not explicit_ws:
|
|
102
|
+
cfg.workspace = str(Path.cwd())
|
|
103
|
+
_auto_workspace_mode(cfg)
|
|
104
|
+
return cfg
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_adhoc(argv):
|
|
108
|
+
"""``painapple --in-docker``: the cwd (or --workspace) as a
|
|
109
|
+
foreground container sandbox. Ctrl-C stops it, --rm cleans up."""
|
|
110
|
+
from painapple_code.cli import profiles
|
|
111
|
+
profiles.ensure_migrated()
|
|
112
|
+
cfg = adhoc_settings(argv)
|
|
113
|
+
return run_container(cfg, detach=False, profile=None)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def profile_settings(prof, argv=()):
|
|
117
|
+
"""DockerSettings for a profile + explicit flag overrides.
|
|
118
|
+
|
|
119
|
+
``mode: docker`` profiles carry the full settings themselves. A
|
|
120
|
+
``mode: host`` profile forced into docker (--in-docker override)
|
|
121
|
+
maps its host values onto container settings — its data home rides
|
|
122
|
+
along as a bind-mounted /data, so it stays the same deployment."""
|
|
123
|
+
if prof.is_docker:
|
|
124
|
+
cfg = docker_settings_with_globals(prof)
|
|
125
|
+
else:
|
|
126
|
+
cfg = DockerSettings(profile=prof.name)
|
|
127
|
+
values = prof.host_values()
|
|
128
|
+
for key, cfg_key in (("host", "LISTEN_HOST"), ("port", "PORT"),
|
|
129
|
+
("tls", "TLS_MODE"), ("instance_name", "INSTANCE_NAME"),
|
|
130
|
+
("accent", "ACCENT"), ("workspace", "WORKSPACE")):
|
|
131
|
+
if values.get(key) not in (None, ""):
|
|
132
|
+
_try_assign(cfg, cfg_key, values[key], f"profile {prof.name}")
|
|
133
|
+
# Same deployment, containerized: the profile's data home IS the
|
|
134
|
+
# container's /data.
|
|
135
|
+
cfg.data_volume = str(prof.home())
|
|
136
|
+
if not cfg.instance_name:
|
|
137
|
+
cfg.instance_name = prof.name[:12]
|
|
138
|
+
# Global --in-docker defaults still supply runtime/image.
|
|
139
|
+
gvalues, _ = serve_config.load(serve_config.root_home() / "serve.yaml")
|
|
140
|
+
for key, cfg_key in (("runtime", "RUNTIME"),
|
|
141
|
+
("runtime_flags", "RUNTIME_FLAGS"),
|
|
142
|
+
("image", "IMAGE")):
|
|
143
|
+
if gvalues.get(key) not in (None, ""):
|
|
144
|
+
_try_assign(cfg, cfg_key, gvalues[key], "serve.yaml")
|
|
145
|
+
|
|
146
|
+
explicit_ws = _apply_serve_flags(cfg, list(argv))
|
|
147
|
+
if not cfg.workspace and cfg.workspace_mode != "multi":
|
|
148
|
+
cfg.workspace = str(Path.cwd())
|
|
149
|
+
_auto_workspace_mode(cfg)
|
|
150
|
+
elif explicit_ws and prof.is_docker is False:
|
|
151
|
+
_auto_workspace_mode(cfg)
|
|
152
|
+
return cfg
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def run_profile(prof, argv=(), detach=False):
|
|
156
|
+
"""Run a profile as a container (its own mode, or forced by
|
|
157
|
+
--in-docker). Foreground unless ``detach``."""
|
|
158
|
+
if not prof.is_docker:
|
|
159
|
+
warn(f"Profile '{prof.name}' is mode: host — running it in docker for "
|
|
160
|
+
f"this launch (--in-docker). Make it permanent with: "
|
|
161
|
+
f"painapple setup {prof.name}")
|
|
162
|
+
cfg = profile_settings(prof, argv)
|
|
163
|
+
return run_container(cfg, detach=detach, profile=prof.name)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Container runtime resolution (docker/podman) and invocation helpers."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from painapple_code.cli.ui import die
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Labels a painapple image carries about the user it runs as. Images
|
|
14
|
+
# built before the uid-alignment work have none of them — every reader
|
|
15
|
+
# below falls back to probing, so pulled older tags still work.
|
|
16
|
+
APP_UID_LABEL = "io.painapple.app-uid"
|
|
17
|
+
APP_GID_LABEL = "io.painapple.app-gid"
|
|
18
|
+
ADAPT_LABEL = "io.painapple.uid-adapt"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _responds(binary):
|
|
22
|
+
"""Does this runtime actually answer? A `docker` CLI whose daemon
|
|
23
|
+
socket we can't reach is on PATH but useless — picking it means the
|
|
24
|
+
real failure surfaces much later as a nonsense message ("Image not
|
|
25
|
+
found locally", because `image inspect` silently returned nothing).
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
return subprocess.run([binary, "info"], capture_output=True,
|
|
29
|
+
timeout=20).returncode == 0
|
|
30
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def auto_detect():
|
|
35
|
+
"""Prefer docker (more common default); podman as fallback — but
|
|
36
|
+
only pick a runtime that's actually reachable, so a docker CLI with
|
|
37
|
+
no daemon behind it doesn't shadow a working podman."""
|
|
38
|
+
present = [(name, path) for name in ("docker", "podman")
|
|
39
|
+
if (path := shutil.which(name))]
|
|
40
|
+
if not present:
|
|
41
|
+
# No RUNTIME env var here — that's the build wrapper's lever
|
|
42
|
+
# (painapple-docker.sh). The CLI's runtime lives in config.
|
|
43
|
+
die("Neither docker nor podman found in PATH.",
|
|
44
|
+
"Install one, then point painapple at it:\n"
|
|
45
|
+
" painapple setup (global runtime default)\n"
|
|
46
|
+
" painapple profile set NAME RUNTIME=… (one deployment — also "
|
|
47
|
+
"takes an absolute path to a custom binary)")
|
|
48
|
+
for name, path in present:
|
|
49
|
+
if _responds(path):
|
|
50
|
+
return name
|
|
51
|
+
names = ", ".join(name for name, _ in present)
|
|
52
|
+
die(f"Found {names} on PATH, but neither answers — `{present[0][0]} info` failed.",
|
|
53
|
+
"Start the service (`systemctl --user start docker`/`podman.socket`), "
|
|
54
|
+
"check you're in the `docker` group, or run `painapple setup` to point "
|
|
55
|
+
"at the runtime you actually use.")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def detect_runtimes():
|
|
59
|
+
"""[(name, path, version)] for every container runtime found on
|
|
60
|
+
PATH — what the setup wizard lists as concrete choices."""
|
|
61
|
+
found = []
|
|
62
|
+
for name in ("docker", "podman"):
|
|
63
|
+
path = shutil.which(name)
|
|
64
|
+
if not path:
|
|
65
|
+
continue
|
|
66
|
+
version = ""
|
|
67
|
+
try:
|
|
68
|
+
out = subprocess.run([path, "--version"], capture_output=True,
|
|
69
|
+
text=True, encoding="utf-8", errors="replace", timeout=5).stdout.strip()
|
|
70
|
+
m = re.search(r"version\s+v?([0-9][\w.-]*)", out)
|
|
71
|
+
version = m.group(1).rstrip(",") if m else out
|
|
72
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
73
|
+
pass
|
|
74
|
+
found.append((name, path, version))
|
|
75
|
+
return found
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Runtime:
|
|
79
|
+
"""One resolved runtime: binary (name or explicit path) + persistent
|
|
80
|
+
global flags.
|
|
81
|
+
|
|
82
|
+
Every invocation goes through here so RUNTIME_FLAGS injection applies
|
|
83
|
+
uniformly — otherwise `run` would see the flags but `ps`/`exec`
|
|
84
|
+
would silently use the default storage driver and miss images.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, cfg):
|
|
88
|
+
self.binary = cfg.runtime or auto_detect()
|
|
89
|
+
# Display/behavior name: an explicit /path/to/podman still counts
|
|
90
|
+
# as podman for --userns/SELinux handling.
|
|
91
|
+
self.name = Path(self.binary).name
|
|
92
|
+
self.flags = shlex.split(cfg.runtime_flags) if cfg.runtime_flags else []
|
|
93
|
+
self._image_ids = {} # image → (uid, gid), memoized probe results
|
|
94
|
+
|
|
95
|
+
def argv(self, *args):
|
|
96
|
+
return [self.binary, *self.flags, *args]
|
|
97
|
+
|
|
98
|
+
def run(self, *args, **kwargs):
|
|
99
|
+
return subprocess.run(self.argv(*args), **kwargs)
|
|
100
|
+
|
|
101
|
+
def output(self, *args):
|
|
102
|
+
"""Stdout of a runtime call, or '' on any failure."""
|
|
103
|
+
try:
|
|
104
|
+
proc = self.run(*args, capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
105
|
+
return proc.stdout if proc.returncode == 0 else ""
|
|
106
|
+
except OSError:
|
|
107
|
+
return ""
|
|
108
|
+
|
|
109
|
+
def ok(self, *args):
|
|
110
|
+
try:
|
|
111
|
+
return self.run(*args, capture_output=True).returncode == 0
|
|
112
|
+
except OSError:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
def exec_interactive(self, *args):
|
|
116
|
+
"""Replace this process with the runtime call — full TTY handoff,
|
|
117
|
+
native Ctrl-C, no Python in the signal path."""
|
|
118
|
+
argv = self.argv(*args)
|
|
119
|
+
os.execvp(argv[0], argv)
|
|
120
|
+
|
|
121
|
+
# ── Queries ─────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
def image_exists(self, image):
|
|
124
|
+
return self.ok("image", "inspect", image)
|
|
125
|
+
|
|
126
|
+
def volume_exists(self, volume):
|
|
127
|
+
return self.ok("volume", "inspect", volume)
|
|
128
|
+
|
|
129
|
+
def container_running(self, name):
|
|
130
|
+
names = self.output("ps", "--format", "{{.Names}}").splitlines()
|
|
131
|
+
return name in names
|
|
132
|
+
|
|
133
|
+
def container_exists(self, name):
|
|
134
|
+
names = self.output("ps", "-a", "--format", "{{.Names}}").splitlines()
|
|
135
|
+
return name in names
|
|
136
|
+
|
|
137
|
+
def container_status(self, name):
|
|
138
|
+
for line in self.output("ps", "--format", "{{.Names}}\t{{.Status}}").splitlines():
|
|
139
|
+
cname, _, status = line.partition("\t")
|
|
140
|
+
if cname == name:
|
|
141
|
+
return status
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def selinux_enforcing(self):
|
|
145
|
+
"""Podman bind mounts need :Z under SELinux."""
|
|
146
|
+
if self.name != "podman" or not shutil.which("selinuxenabled"):
|
|
147
|
+
return False
|
|
148
|
+
try:
|
|
149
|
+
return subprocess.run(["selinuxenabled"], capture_output=True).returncode == 0
|
|
150
|
+
except OSError:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
def responds(self):
|
|
154
|
+
"""Runtime reachable? (Explicitly configured binaries skip
|
|
155
|
+
auto_detect's check, so run_container asks again.)"""
|
|
156
|
+
return _responds(self.binary)
|
|
157
|
+
|
|
158
|
+
# ── Image identity ──────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
def label(self, image, name):
|
|
161
|
+
"""One image label, '' when absent. Docker renders a missing key
|
|
162
|
+
as '<no value>', podman as an empty string — normalize both."""
|
|
163
|
+
raw = self.output("image", "inspect", image, "--format",
|
|
164
|
+
'{{index .Config.Labels "%s"}}' % name).strip()
|
|
165
|
+
return "" if raw in ("<no value>", "<nil>") else raw
|
|
166
|
+
|
|
167
|
+
def image_adapts_uid(self, image):
|
|
168
|
+
"""Does the image's entrypoint re-stamp its user to match host
|
|
169
|
+
ownership and drop privileges? (Images predating that: no.)"""
|
|
170
|
+
return self.label(image, ADAPT_LABEL) == "1"
|
|
171
|
+
|
|
172
|
+
def image_user_ids(self, image):
|
|
173
|
+
"""(uid, gid) of the image's `app` user.
|
|
174
|
+
|
|
175
|
+
Labels first — an adapting image starts as root, so its
|
|
176
|
+
Config.User says nothing about the uid the server ends up
|
|
177
|
+
running as. Falls back to one throwaway container for images
|
|
178
|
+
built before the labels existed.
|
|
179
|
+
"""
|
|
180
|
+
if image in self._image_ids:
|
|
181
|
+
return self._image_ids[image]
|
|
182
|
+
|
|
183
|
+
uid, gid = self.label(image, APP_UID_LABEL), self.label(image, APP_GID_LABEL)
|
|
184
|
+
if uid.isdigit() and gid.isdigit():
|
|
185
|
+
ids = (int(uid), int(gid))
|
|
186
|
+
else:
|
|
187
|
+
out = self.output("run", "--rm", "--entrypoint", "", "--network", "none",
|
|
188
|
+
image, "sh", "-c", "id -u; id -g").split()
|
|
189
|
+
ids = ((int(out[0]), int(out[1]))
|
|
190
|
+
if len(out) >= 2 and all(t.isdigit() for t in out[:2]) else (None, None))
|
|
191
|
+
self._image_ids[image] = ids
|
|
192
|
+
return ids
|
|
193
|
+
|
|
194
|
+
def supports_userns_map(self):
|
|
195
|
+
"""podman ≥ 4.3 understands `keep-id:uid=…,gid=…` (the flag that
|
|
196
|
+
lands the host user ON the image's app uid). Unparsable version
|
|
197
|
+
→ assume yes; podman 4.3 shipped in 2022 and the failure is a
|
|
198
|
+
loud one-liner from podman itself, not a silent misbehavior."""
|
|
199
|
+
if self.name != "podman":
|
|
200
|
+
return False
|
|
201
|
+
raw = self.output("version", "--format", "{{.Client.Version}}").strip()
|
|
202
|
+
m = re.match(r"(\d+)\.(\d+)", raw)
|
|
203
|
+
return (int(m.group(1)), int(m.group(2))) >= (4, 3) if m else True
|
|
204
|
+
|
|
205
|
+
# ── Volume ownership ────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
def chown_volume(self, image, volume, uid, gid, extra_args=()):
|
|
208
|
+
"""Make a named volume owned by the uid the container will run
|
|
209
|
+
as — as root inside the same user namespace the real run uses,
|
|
210
|
+
so the mapping matches.
|
|
211
|
+
|
|
212
|
+
Testing the top-level dir is NOT enough: a volume seeded from the
|
|
213
|
+
image has its root owned by `app` while everything a previous
|
|
214
|
+
container wrote under it (/data/logs/server.log, the classic) is
|
|
215
|
+
owned by that container's uid instead. `-print -quit` stops at
|
|
216
|
+
the first offender, so this is one cheap walk when there's
|
|
217
|
+
nothing to fix — `chown -R` over a populated data home (DuckDB
|
|
218
|
+
plus every session log) is not something to redo on every start.
|
|
219
|
+
"""
|
|
220
|
+
script = (f'[ -n "$(find /vol \\( ! -uid {uid} -o ! -gid {gid} \\) '
|
|
221
|
+
f'-print -quit 2>/dev/null)" ] && chown -R {uid}:{gid} /vol; :')
|
|
222
|
+
return self.ok("run", "--rm", "--user", "0", "--entrypoint", "",
|
|
223
|
+
*extra_args, "-v", f"{volume}:/vol", image, "sh", "-c", script)
|