code-puppy-core-plugins 0.0.2__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.
- code_puppy_core_plugins/__init__.py +5 -0
- code_puppy_core_plugins/acp/README.md +266 -0
- code_puppy_core_plugins/acp/__init__.py +8 -0
- code_puppy_core_plugins/acp/agent.py +494 -0
- code_puppy_core_plugins/acp/bridge.py +309 -0
- code_puppy_core_plugins/acp/capabilities.py +73 -0
- code_puppy_core_plugins/acp/commands.py +116 -0
- code_puppy_core_plugins/acp/content.py +115 -0
- code_puppy_core_plugins/acp/io_delegation.py +231 -0
- code_puppy_core_plugins/acp/mcp_config.py +66 -0
- code_puppy_core_plugins/acp/permissions.py +159 -0
- code_puppy_core_plugins/acp/persistence.py +197 -0
- code_puppy_core_plugins/acp/register_callbacks.py +110 -0
- code_puppy_core_plugins/acp/replay.py +114 -0
- code_puppy_core_plugins/acp/session.py +329 -0
- code_puppy_core_plugins/acp/session_config.py +179 -0
- code_puppy_core_plugins/acp/state.py +135 -0
- code_puppy_core_plugins/agent_skills/__init__.py +22 -0
- code_puppy_core_plugins/agent_skills/config.py +208 -0
- code_puppy_core_plugins/agent_skills/discovery.py +337 -0
- code_puppy_core_plugins/agent_skills/downloader.py +392 -0
- code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
- code_puppy_core_plugins/agent_skills/installer.py +22 -0
- code_puppy_core_plugins/agent_skills/metadata.py +219 -0
- code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
- code_puppy_core_plugins/agent_skills/provider.py +52 -0
- code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
- code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
- code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
- code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
- code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
- code_puppy_core_plugins/aws_bedrock/config.py +101 -0
- code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
- code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
- code_puppy_core_plugins/azure_foundry/README.md +238 -0
- code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
- code_puppy_core_plugins/azure_foundry/config.py +126 -0
- code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
- code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
- code_puppy_core_plugins/azure_foundry/token.py +182 -0
- code_puppy_core_plugins/azure_foundry/utils.py +375 -0
- code_puppy_core_plugins/btw/__init__.py +1 -0
- code_puppy_core_plugins/btw/inline_view.py +135 -0
- code_puppy_core_plugins/btw/register_callbacks.py +106 -0
- code_puppy_core_plugins/btw/side_query.py +91 -0
- code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
- code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
- code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
- code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
- code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
- code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
- code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
- code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
- code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
- code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
- code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
- code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
- code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
- code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
- code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
- code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
- code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
- code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
- code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
- code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
- code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
- code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
- code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
- code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
- code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
- code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
- code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
- code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
- code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
- code_puppy_core_plugins/computer_use/README.md +109 -0
- code_puppy_core_plugins/computer_use/__init__.py +1 -0
- code_puppy_core_plugins/computer_use/accessibility.py +163 -0
- code_puppy_core_plugins/computer_use/activation.py +75 -0
- code_puppy_core_plugins/computer_use/backend.py +584 -0
- code_puppy_core_plugins/computer_use/backend_types.py +5 -0
- code_puppy_core_plugins/computer_use/capture.py +110 -0
- code_puppy_core_plugins/computer_use/commands.py +62 -0
- code_puppy_core_plugins/computer_use/geometry.py +81 -0
- code_puppy_core_plugins/computer_use/inline_image.py +56 -0
- code_puppy_core_plugins/computer_use/keycodes.py +47 -0
- code_puppy_core_plugins/computer_use/policy.py +144 -0
- code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
- code_puppy_core_plugins/computer_use/safety.py +18 -0
- code_puppy_core_plugins/computer_use/settle.py +64 -0
- code_puppy_core_plugins/computer_use/state.py +86 -0
- code_puppy_core_plugins/computer_use/tools.py +469 -0
- code_puppy_core_plugins/context_indicator/__init__.py +14 -0
- code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
- code_puppy_core_plugins/context_indicator/usage.py +40 -0
- code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
- code_puppy_core_plugins/copilot_auth/config.py +93 -0
- code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
- code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
- code_puppy_core_plugins/copilot_auth/utils.py +587 -0
- code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
- code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
- code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
- code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
- code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
- code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
- code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
- code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
- code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
- code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
- code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
- code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
- code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
- code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
- code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
- code_puppy_core_plugins/emoji_filter/config.py +26 -0
- code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
- code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
- code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
- code_puppy_core_plugins/example_custom_command/README.md +328 -0
- code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
- code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
- code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
- code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
- code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
- code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
- code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
- code_puppy_core_plugins/force_push_guard/detector.py +96 -0
- code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
- code_puppy_core_plugins/fork/__init__.py +7 -0
- code_puppy_core_plugins/fork/register_callbacks.py +445 -0
- code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
- code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
- code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
- code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
- code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
- code_puppy_core_plugins/grok_oauth/config.py +49 -0
- code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
- code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
- code_puppy_core_plugins/grok_oauth/utils.py +195 -0
- code_puppy_core_plugins/herdr/README.md +168 -0
- code_puppy_core_plugins/herdr/__init__.py +9 -0
- code_puppy_core_plugins/herdr/client.py +339 -0
- code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
- code_puppy_core_plugins/herdr/reporter.py +212 -0
- code_puppy_core_plugins/herdr/smoke.py +143 -0
- code_puppy_core_plugins/herdr/sources.py +133 -0
- code_puppy_core_plugins/hook_creator/__init__.py +1 -0
- code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy_core_plugins/hook_manager/__init__.py +1 -0
- code_puppy_core_plugins/hook_manager/config.py +290 -0
- code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
- code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
- code_puppy_core_plugins/meta_oauth/config.py +55 -0
- code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
- code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
- code_puppy_core_plugins/meta_oauth/utils.py +252 -0
- code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
- code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
- code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
- code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
- code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
- code_puppy_core_plugins/no_tools/__init__.py +0 -0
- code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
- code_puppy_core_plugins/oauth_pasteback.py +110 -0
- code_puppy_core_plugins/oauth_puppy_html.py +194 -0
- code_puppy_core_plugins/obsidian_agent/README.md +52 -0
- code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
- code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
- code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
- code_puppy_core_plugins/ollama/__init__.py +1 -0
- code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
- code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
- code_puppy_core_plugins/ollama_setup/completer.py +38 -0
- code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
- code_puppy_core_plugins/plugin_list/__init__.py +0 -0
- code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
- code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
- code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
- code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
- code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
- code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
- code_puppy_core_plugins/pop_command/__init__.py +1 -0
- code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
- code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
- code_puppy_core_plugins/prompt_newline/config.py +21 -0
- code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
- code_puppy_core_plugins/prune/__init__.py +1 -0
- code_puppy_core_plugins/prune/prune_menu.py +308 -0
- code_puppy_core_plugins/prune/prune_model.py +517 -0
- code_puppy_core_plugins/prune/prune_render.py +354 -0
- code_puppy_core_plugins/prune/register_callbacks.py +388 -0
- code_puppy_core_plugins/puppy_kennel/README.md +203 -0
- code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
- code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
- code_puppy_core_plugins/puppy_kennel/config.py +36 -0
- code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
- code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
- code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
- code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
- code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
- code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
- code_puppy_core_plugins/puppy_kennel/state.py +68 -0
- code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
- code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
- code_puppy_core_plugins/puppy_spinner/README.md +93 -0
- code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
- code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
- code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
- code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
- code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
- code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
- code_puppy_core_plugins/quick_resume/__init__.py +0 -0
- code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
- code_puppy_core_plugins/review_pr/__init__.py +1 -0
- code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
- code_puppy_core_plugins/shell_safety/__init__.py +6 -0
- code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
- code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
- code_puppy_core_plugins/stack_dump/__init__.py +1 -0
- code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
- code_puppy_core_plugins/statusline/__init__.py +14 -0
- code_puppy_core_plugins/statusline/config.py +68 -0
- code_puppy_core_plugins/statusline/payload.py +138 -0
- code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
- code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
- code_puppy_core_plugins/statusline/runner.py +122 -0
- code_puppy_core_plugins/statusline/statusline_command.py +197 -0
- code_puppy_core_plugins/steer_queue/__init__.py +10 -0
- code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
- code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
- code_puppy_core_plugins/subagent_panel/README.md +102 -0
- code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
- code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
- code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
- code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
- code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
- code_puppy_core_plugins/subagent_panel/state.py +231 -0
- code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
- code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
- code_puppy_core_plugins/theme/README.md +86 -0
- code_puppy_core_plugins/theme/__init__.py +14 -0
- code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
- code_puppy_core_plugins/theme/content_styles.py +146 -0
- code_puppy_core_plugins/theme/osc_palette.py +178 -0
- code_puppy_core_plugins/theme/picker.py +358 -0
- code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
- code_puppy_core_plugins/theme/register_callbacks.py +348 -0
- code_puppy_core_plugins/theme/rich_themes.py +202 -0
- code_puppy_core_plugins/theme/themes.py +720 -0
- code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
- code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
- code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
- code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
- code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
- code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
- code_puppy_core_plugins/universal_constructor/models.py +138 -0
- code_puppy_core_plugins/universal_constructor/provider.py +62 -0
- code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
- code_puppy_core_plugins/universal_constructor/registry.py +302 -0
- code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
- code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
- code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
- code_puppy_core_plugins/wiggum/__init__.py +1 -0
- code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
- code_puppy_core_plugins/wiggum/judge.py +299 -0
- code_puppy_core_plugins/wiggum/judge_config.py +249 -0
- code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
- code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
- code_puppy_core_plugins/wiggum/state.py +89 -0
- code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
- code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
- code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
- code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
- code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
- code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
- code_puppy_core_plugins-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render a colorized /flux/status panel for the terminal.
|
|
3
|
+
|
|
4
|
+
This is the code-puppy equivalent of Wibey's native `ui-mode: flux-status`
|
|
5
|
+
renderer. Wibey draws an interactive Ink overlay; code-puppy can't host an
|
|
6
|
+
interactive UI, so this script prints a static but fully-colored panel.
|
|
7
|
+
|
|
8
|
+
Data model (all under ~/.flux/<flattened-cwd>/):
|
|
9
|
+
todo/*.md -> frontmatter `stage` + `status`
|
|
10
|
+
done/<timestamp>/*.md -> completed tasks, grouped by run timestamp
|
|
11
|
+
review/<severity>/*.md -> severity is the folder name
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
flux_status.py [--base DIR] [--sections todo,done,review] [--no-color]
|
|
15
|
+
|
|
16
|
+
If --base is omitted, the flux base is derived from the current working
|
|
17
|
+
directory exactly the way the //flux commands do it (every run of
|
|
18
|
+
non-alphanumeric chars in the absolute path becomes a single hyphen).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
# --- ANSI palette -----------------------------------------------------------
|
|
30
|
+
RESET = "\033[0m"
|
|
31
|
+
BOLD = "\033[1m"
|
|
32
|
+
DIM = "\033[2m"
|
|
33
|
+
MAGENTA = "\033[1;35m"
|
|
34
|
+
CYAN = "\033[36m"
|
|
35
|
+
GREEN = "\033[32m"
|
|
36
|
+
RED = "\033[31m"
|
|
37
|
+
ORANGE = "\033[38;5;208m"
|
|
38
|
+
TEAL = "\033[38;5;43m"
|
|
39
|
+
GREY = "\033[90m"
|
|
40
|
+
WHITE = "\033[1;37m"
|
|
41
|
+
|
|
42
|
+
# --- glyphs (escaped so emoji-stripping file writers leave them intact) -----
|
|
43
|
+
FRAKTUR_F = "\U0001d571" # mathematical bold fraktur capital F
|
|
44
|
+
ICON_PROGRESS = "\U0001f504" # clockwise arrows
|
|
45
|
+
ICON_DONE = "\u2705" # white heavy check mark on green
|
|
46
|
+
ICON_REWORK = "\U0001f501" # repeat
|
|
47
|
+
DOT = "\u25cf" # solid circle
|
|
48
|
+
|
|
49
|
+
STATUS_STYLE = {
|
|
50
|
+
"in-progress": (ICON_PROGRESS, ORANGE),
|
|
51
|
+
"needs-rework": (ICON_REWORK, RED),
|
|
52
|
+
"done": (ICON_DONE, GREEN),
|
|
53
|
+
"completed": (ICON_DONE, GREEN),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
SEVERITIES = ("critical", "high", "medium", "low")
|
|
57
|
+
SEVERITY_COLOR = {
|
|
58
|
+
"critical": RED,
|
|
59
|
+
"high": ORANGE,
|
|
60
|
+
"medium": TEAL,
|
|
61
|
+
"low": GREEN,
|
|
62
|
+
}
|
|
63
|
+
# column header label -> fixed display width for the review grid
|
|
64
|
+
SEV_COL_WIDTH = {"critical": 10, "high": 6, "medium": 8, "low": 5}
|
|
65
|
+
|
|
66
|
+
# Panel layout constants (named so a column-width tweak can't silently break
|
|
67
|
+
# the header/rule alignment).
|
|
68
|
+
_SECTION_PAD = 18 # extra width added to name_w + stage_w for section rules
|
|
69
|
+
_MIN_PANEL_W = 48 # floor so short content still frames nicely
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Theme:
|
|
73
|
+
"""Wraps color usage so --no-color collapses every code to ''."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, enabled: bool) -> None:
|
|
76
|
+
self.enabled = enabled
|
|
77
|
+
|
|
78
|
+
def __call__(self, code: str, text: str) -> str:
|
|
79
|
+
if not self.enabled:
|
|
80
|
+
return text
|
|
81
|
+
return f"{code}{text}{RESET}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def flatten_cwd(cwd: str) -> str:
|
|
85
|
+
"""Replace runs of non-alphanumerics with single hyphens (flux convention)."""
|
|
86
|
+
return re.sub(r"[^a-zA-Z0-9]+", "-", cwd)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def derive_base(explicit: str | None) -> Path:
|
|
90
|
+
if explicit:
|
|
91
|
+
return Path(explicit).expanduser()
|
|
92
|
+
home = Path.home()
|
|
93
|
+
return home / ".flux" / flatten_cwd(os.getcwd())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_frontmatter(path: Path) -> dict[str, str]:
|
|
97
|
+
"""Read the leading `--- ... ---` YAML-ish block into a flat dict."""
|
|
98
|
+
data: dict[str, str] = {}
|
|
99
|
+
try:
|
|
100
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
101
|
+
except OSError:
|
|
102
|
+
return data
|
|
103
|
+
if not text.startswith("---"):
|
|
104
|
+
return data
|
|
105
|
+
lines = text.splitlines()
|
|
106
|
+
for line in lines[1:]:
|
|
107
|
+
if line.strip() == "---":
|
|
108
|
+
break
|
|
109
|
+
if ":" in line:
|
|
110
|
+
key, _, val = line.partition(":")
|
|
111
|
+
data[key.strip()] = val.strip()
|
|
112
|
+
return data
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def status_cell(status: str, theme: Theme) -> str:
|
|
116
|
+
icon, color = STATUS_STYLE.get(status, ("", GREY))
|
|
117
|
+
label = theme(color, status) if status else theme(GREY, "(unknown)")
|
|
118
|
+
return f"{icon} {label}".strip() if icon else label
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def rule(width: int, char: str, color: str, theme: Theme) -> str:
|
|
122
|
+
return theme(color, char * width)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def visible_len(name: str) -> int:
|
|
126
|
+
return len(name)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def collect_todos(base: Path) -> list[tuple[str, str, str]]:
|
|
130
|
+
rows = []
|
|
131
|
+
todo = base / "todo"
|
|
132
|
+
if not todo.is_dir():
|
|
133
|
+
return rows
|
|
134
|
+
for md in sorted(todo.glob("*.md")):
|
|
135
|
+
fm = parse_frontmatter(md)
|
|
136
|
+
rows.append((md.stem, fm.get("stage", ""), fm.get("status", "")))
|
|
137
|
+
return rows
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def collect_done(base: Path) -> list[tuple[str, list[tuple[str, str, str]]]]:
|
|
141
|
+
"""Returns [(timestamp_label, [(name, stage, status), ...]), ...]."""
|
|
142
|
+
groups = []
|
|
143
|
+
done = base / "done"
|
|
144
|
+
if not done.is_dir():
|
|
145
|
+
return groups
|
|
146
|
+
for ts_dir in sorted(done.iterdir(), reverse=True):
|
|
147
|
+
if not ts_dir.is_dir():
|
|
148
|
+
continue
|
|
149
|
+
rows = []
|
|
150
|
+
for md in sorted(ts_dir.glob("*.md")):
|
|
151
|
+
fm = parse_frontmatter(md)
|
|
152
|
+
rows.append((md.stem, fm.get("stage", ""), fm.get("status", "completed")))
|
|
153
|
+
if rows:
|
|
154
|
+
groups.append((format_timestamp(ts_dir.name), rows))
|
|
155
|
+
return groups
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def format_timestamp(dirname: str) -> str:
|
|
159
|
+
"""`2026-04-29-16-57` -> `2026-04-29 16:57`; pass through anything odd."""
|
|
160
|
+
parts = dirname.split("-")
|
|
161
|
+
if len(parts) == 5:
|
|
162
|
+
return f"{parts[0]}-{parts[1]}-{parts[2]} {parts[3]}:{parts[4]}"
|
|
163
|
+
return dirname
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def collect_reviews(base: Path) -> list[tuple[str, str]]:
|
|
167
|
+
"""Returns [(review_name, severity), ...] ordered critical->low."""
|
|
168
|
+
out = []
|
|
169
|
+
review = base / "review"
|
|
170
|
+
if not review.is_dir():
|
|
171
|
+
return out
|
|
172
|
+
for sev in SEVERITIES:
|
|
173
|
+
sev_dir = review / sev
|
|
174
|
+
if not sev_dir.is_dir():
|
|
175
|
+
continue
|
|
176
|
+
for md in sorted(sev_dir.glob("*.md")):
|
|
177
|
+
out.append((md.stem, sev))
|
|
178
|
+
return out
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def render(base: Path, sections: set[str], theme: Theme) -> str:
|
|
182
|
+
todos = collect_todos(base) if "todo" in sections else []
|
|
183
|
+
done_groups = collect_done(base) if "done" in sections else []
|
|
184
|
+
reviews = collect_reviews(base) if "review" in sections else []
|
|
185
|
+
|
|
186
|
+
# column width across everything we will print on the left
|
|
187
|
+
names = [r[0] for r in todos]
|
|
188
|
+
names += [r[0] for g in done_groups for r in g[1]]
|
|
189
|
+
names += [r[0] for r in reviews]
|
|
190
|
+
name_w = max([visible_len(n) for n in names] + [len("TODO-FILE")]) + 2
|
|
191
|
+
name_w = min(name_w, 50)
|
|
192
|
+
stage_w = 8
|
|
193
|
+
total_w = max(name_w + stage_w + _SECTION_PAD, _MIN_PANEL_W)
|
|
194
|
+
|
|
195
|
+
lines: list[str] = []
|
|
196
|
+
lines.append(theme(MAGENTA, f"{FRAKTUR_F} FLUX STATUS"))
|
|
197
|
+
lines.append(rule(total_w, "\u2550", MAGENTA, theme))
|
|
198
|
+
rendered_any = False # have we emitted a body section yet?
|
|
199
|
+
|
|
200
|
+
# --- TODO section -------------------------------------------------------
|
|
201
|
+
if "todo" in sections:
|
|
202
|
+
rendered_any = True
|
|
203
|
+
header = (
|
|
204
|
+
theme(WHITE, "TODO-FILE".ljust(name_w))
|
|
205
|
+
+ theme(WHITE, "STAGE".ljust(stage_w))
|
|
206
|
+
+ theme(WHITE, "STATUS")
|
|
207
|
+
)
|
|
208
|
+
lines.append("")
|
|
209
|
+
lines.append(header)
|
|
210
|
+
lines.append(rule(total_w, "\u2500", GREY, theme))
|
|
211
|
+
if not todos:
|
|
212
|
+
lines.append(theme(GREY, "(no todos)"))
|
|
213
|
+
for name, stage, status in todos:
|
|
214
|
+
lines.append(
|
|
215
|
+
theme(CYAN, name.ljust(name_w))
|
|
216
|
+
+ stage.ljust(stage_w)
|
|
217
|
+
+ status_cell(status, theme)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# --- COMPLETED section --------------------------------------------------
|
|
221
|
+
if "done" in sections and done_groups:
|
|
222
|
+
lines.append("")
|
|
223
|
+
if rendered_any:
|
|
224
|
+
lines.append(rule(total_w, "\u2550", MAGENTA, theme))
|
|
225
|
+
rendered_any = True
|
|
226
|
+
lines.append(theme(MAGENTA, "COMPLETED TASKS"))
|
|
227
|
+
lines.append("")
|
|
228
|
+
lines.append(
|
|
229
|
+
theme(WHITE, "TASK-FILE".ljust(name_w))
|
|
230
|
+
+ theme(WHITE, "STAGE".ljust(stage_w))
|
|
231
|
+
+ theme(WHITE, "STATUS")
|
|
232
|
+
)
|
|
233
|
+
for ts_label, rows in done_groups:
|
|
234
|
+
lines.append(theme(GREY, f"\u2500\u2500 {ts_label} \u2500\u2500"))
|
|
235
|
+
for name, stage, status in rows:
|
|
236
|
+
lines.append(
|
|
237
|
+
theme(CYAN, name.ljust(name_w))
|
|
238
|
+
+ stage.ljust(stage_w)
|
|
239
|
+
+ status_cell(status, theme)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# --- REVIEW section -----------------------------------------------------
|
|
243
|
+
if "review" in sections and reviews:
|
|
244
|
+
lines.append("")
|
|
245
|
+
if rendered_any:
|
|
246
|
+
lines.append(rule(total_w, "\u2550", MAGENTA, theme))
|
|
247
|
+
rendered_any = True
|
|
248
|
+
lines.append(theme(ORANGE, "REVIEW TASKS"))
|
|
249
|
+
lines.append("")
|
|
250
|
+
# column header
|
|
251
|
+
head = theme(WHITE, "REVIEW-FILE".ljust(name_w))
|
|
252
|
+
for sev in SEVERITIES:
|
|
253
|
+
head += theme(SEVERITY_COLOR[sev], sev.upper().ljust(SEV_COL_WIDTH[sev]))
|
|
254
|
+
lines.append(head)
|
|
255
|
+
# Underline width = ACTUAL review columns (name + severities), not total_w,
|
|
256
|
+
# or the rule drifts short whenever SEV_COL_WIDTH changes.
|
|
257
|
+
review_w = max(name_w + sum(SEV_COL_WIDTH[s] for s in SEVERITIES), _MIN_PANEL_W)
|
|
258
|
+
lines.append(rule(review_w, "\u2500", GREY, theme))
|
|
259
|
+
for name, sev in reviews:
|
|
260
|
+
row = theme(CYAN, name.ljust(name_w))
|
|
261
|
+
for col in SEVERITIES:
|
|
262
|
+
if col == sev:
|
|
263
|
+
cell = theme(SEVERITY_COLOR[sev], DOT)
|
|
264
|
+
row += cell + " " * (SEV_COL_WIDTH[col] - 1)
|
|
265
|
+
else:
|
|
266
|
+
row += " " * SEV_COL_WIDTH[col]
|
|
267
|
+
lines.append(row.rstrip())
|
|
268
|
+
|
|
269
|
+
lines.append(rule(total_w, "\u2550", MAGENTA, theme))
|
|
270
|
+
return "\n".join(lines)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def force_utf8_stdout() -> None:
|
|
274
|
+
"""Reconfigure stdout to UTF-8 so emoji survive legacy Windows codepages.
|
|
275
|
+
|
|
276
|
+
Under the exec runner the env already forces UTF-8 (PYTHONIOENCODING);
|
|
277
|
+
this covers standalone runs in a cp1252 console, where printing a single
|
|
278
|
+
emoji or box-drawing glyph would otherwise raise UnicodeEncodeError.
|
|
279
|
+
Best-effort by design -- these scripts must never crash on an IO quirk.
|
|
280
|
+
(Duplicated across the flux_* scripts on purpose: each is a standalone,
|
|
281
|
+
dependency-free file.)
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def main(argv: list[str] | None = None) -> int:
|
|
290
|
+
force_utf8_stdout()
|
|
291
|
+
parser = argparse.ArgumentParser(description="Render /flux/status panel")
|
|
292
|
+
parser.add_argument("--base", default=None, help="Explicit flux base dir")
|
|
293
|
+
parser.add_argument(
|
|
294
|
+
"--sections",
|
|
295
|
+
default="todo,done,review",
|
|
296
|
+
help="Comma list of: todo,done,review (default: all)",
|
|
297
|
+
)
|
|
298
|
+
parser.add_argument(
|
|
299
|
+
"sections_pos",
|
|
300
|
+
nargs="*",
|
|
301
|
+
default=[],
|
|
302
|
+
help="Optional positional section filter (todo/done/review); overrides --sections.",
|
|
303
|
+
)
|
|
304
|
+
parser.add_argument("--no-color", action="store_true", help="Disable ANSI color")
|
|
305
|
+
args = parser.parse_args(argv)
|
|
306
|
+
|
|
307
|
+
# valid_sections by hand, not argparse `choices=`: choices + nargs='*' rejects the
|
|
308
|
+
# empty-list default. Validated below after merging both input paths.
|
|
309
|
+
valid_sections = {"todo", "done", "review"}
|
|
310
|
+
|
|
311
|
+
base = derive_base(args.base)
|
|
312
|
+
# Positional section args (e.g. `flux_status.py todo review`) override
|
|
313
|
+
# --sections so command-line callers don't need to remember the flag name.
|
|
314
|
+
if args.sections_pos:
|
|
315
|
+
sections = {s.strip() for s in args.sections_pos if s.strip()}
|
|
316
|
+
else:
|
|
317
|
+
sections = {s.strip() for s in args.sections.split(",") if s.strip()}
|
|
318
|
+
|
|
319
|
+
# Validate the resolved set once so a typo via EITHER input path (positional
|
|
320
|
+
# or --sections) fails loudly instead of silently rendering a blank panel.
|
|
321
|
+
bad = sorted(sections - valid_sections)
|
|
322
|
+
if bad:
|
|
323
|
+
parser.error(
|
|
324
|
+
f"invalid section(s): {', '.join(bad)} "
|
|
325
|
+
f"(choose from {', '.join(sorted(valid_sections))})"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# The exec runner pipes stdout + injects FORCE_COLOR=1 (see _run_exec_directive), so
|
|
329
|
+
# color stays on despite isatty()==False. Keep the FORCE_COLOR branch so panels
|
|
330
|
+
# don't silently go monochrome under the runner.
|
|
331
|
+
color_on = not args.no_color and (
|
|
332
|
+
sys.stdout.isatty() or os.environ.get("FORCE_COLOR")
|
|
333
|
+
)
|
|
334
|
+
theme = Theme(bool(color_on))
|
|
335
|
+
|
|
336
|
+
if not base.exists():
|
|
337
|
+
print(theme(GREY, f"(no flux state at {base})"))
|
|
338
|
+
return 0
|
|
339
|
+
|
|
340
|
+
print(render(base, sections, theme))
|
|
341
|
+
return 0
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
if __name__ == "__main__":
|
|
345
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Install the bundled Flux command set into ``~/.code_puppy``.
|
|
2
|
+
|
|
3
|
+
The plugin ships a snapshot of the Flux slash-command suite (markdown command
|
|
4
|
+
files + a couple of ``exec:`` helper scripts) as package data under
|
|
5
|
+
``bundled/``. On a fresh install -- or after code-puppy is upgraded -- we copy
|
|
6
|
+
that payload into the user config dir so ``/flux/...`` commands are available
|
|
7
|
+
out of the box.
|
|
8
|
+
|
|
9
|
+
Design goals:
|
|
10
|
+
|
|
11
|
+
* **Idempotent.** Re-running with the same bundled content is a no-op.
|
|
12
|
+
* **Non-destructive.** A file the user has hand-edited is never silently
|
|
13
|
+
clobbered -- it is backed up to ``<name>.bak`` before the fresh copy lands.
|
|
14
|
+
We tell "user edited it" from "we wrote it last time" via a manifest of the
|
|
15
|
+
SHA-256 hashes we installed (``.flux_bootstrap_manifest.json``).
|
|
16
|
+
* **Version-gated.** We only walk the tree when the installed code-puppy
|
|
17
|
+
version differs from the marker we stored last run, so steady-state startup
|
|
18
|
+
stays cheap.
|
|
19
|
+
* **Fails closed, never fatal.** Any error is swallowed by the caller; a broken
|
|
20
|
+
install must not take down the whole app.
|
|
21
|
+
|
|
22
|
+
The manifest + version marker are dot-files, so they never register as
|
|
23
|
+
slash-commands and stay out of the way of the command loader.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import shutil
|
|
32
|
+
import stat
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Dict, List
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
import fcntl
|
|
39
|
+
except ImportError: # pragma: no cover - Windows has no fcntl
|
|
40
|
+
fcntl = None # type: ignore[assignment]
|
|
41
|
+
|
|
42
|
+
# Best-effort cross-process lock so two instances can't both walk/copy the tree during
|
|
43
|
+
# the one-time install window (torn reads, spurious .bak). Dot-file -> loader ignores it.
|
|
44
|
+
LOCK_NAME = ".flux_bootstrap.lock"
|
|
45
|
+
|
|
46
|
+
# Directory holding the shipped payload (``bundled/commands`` + ``bundled/scripts``).
|
|
47
|
+
BUNDLED_DIR = Path(__file__).parent / "bundled"
|
|
48
|
+
|
|
49
|
+
# Dot-files live at the root of the config dir so the command loader (which
|
|
50
|
+
# skips ``.``-prefixed namespaces) never picks them up.
|
|
51
|
+
VERSION_MARKER_NAME = ".flux_bootstrap_version"
|
|
52
|
+
MANIFEST_NAME = ".flux_bootstrap_manifest.json"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class InstallReport:
|
|
57
|
+
"""Summary of what an install pass did (handy for logging + tests)."""
|
|
58
|
+
|
|
59
|
+
installed: List[str] = field(default_factory=list)
|
|
60
|
+
updated: List[str] = field(default_factory=list)
|
|
61
|
+
backed_up: List[str] = field(default_factory=list)
|
|
62
|
+
skipped: List[str] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def changed(self) -> bool:
|
|
66
|
+
return bool(self.installed or self.updated or self.backed_up)
|
|
67
|
+
|
|
68
|
+
def summary(self) -> str:
|
|
69
|
+
return (
|
|
70
|
+
f"{len(self.installed)} new, {len(self.updated)} updated, "
|
|
71
|
+
f"{len(self.backed_up)} backed up, {len(self.skipped)} unchanged"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _sha256(path: Path) -> str:
|
|
76
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sha256_bytes(data: bytes) -> str:
|
|
80
|
+
return hashlib.sha256(data).hexdigest()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _atomic_write_text(path: Path, text: str) -> None:
|
|
84
|
+
"""Write ``text`` to ``path`` via a temp file + atomic replace.
|
|
85
|
+
|
|
86
|
+
A crash mid-write leaves the previous file intact (or absent) rather than a
|
|
87
|
+
half-written, corrupt one. ``os.replace`` is atomic on the same filesystem.
|
|
88
|
+
"""
|
|
89
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
90
|
+
tmp.write_text(text, encoding="utf-8")
|
|
91
|
+
os.replace(tmp, path)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _write_bytes_preserving_mode(dest: Path, payload: bytes, src: Path) -> None:
|
|
95
|
+
"""Write ``payload`` to ``dest`` atomically and copy ``src``'s mode bits.
|
|
96
|
+
|
|
97
|
+
Writes to a temp file, chmods it to match ``src`` (executable/mode metadata
|
|
98
|
+
on installed scripts so a future direct-exec doesn't lose the +x bit), then
|
|
99
|
+
atomically replaces ``dest``. A crash mid-write leaves the previous file
|
|
100
|
+
intact rather than a truncated one -- the same crash-safety the manifest and
|
|
101
|
+
version marker already get via :func:`_atomic_write_text`. Chmod failures
|
|
102
|
+
are non-fatal; the content landing is what matters.
|
|
103
|
+
"""
|
|
104
|
+
tmp = dest.with_name(dest.name + ".tmp")
|
|
105
|
+
tmp.write_bytes(payload)
|
|
106
|
+
try:
|
|
107
|
+
tmp.chmod(stat.S_IMODE(src.stat().st_mode))
|
|
108
|
+
except OSError:
|
|
109
|
+
pass
|
|
110
|
+
os.replace(tmp, dest)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _unique_backup_path(dest: Path) -> Path:
|
|
114
|
+
"""Return a non-colliding ``<name>.bak`` (or ``.bak.1``, ``.bak.2``, ...).
|
|
115
|
+
|
|
116
|
+
Successive version bumps against a repeatedly hand-edited file would
|
|
117
|
+
otherwise clobber the previous backup. Find the first free slot instead.
|
|
118
|
+
"""
|
|
119
|
+
backup = dest.with_name(dest.name + ".bak")
|
|
120
|
+
if not backup.exists():
|
|
121
|
+
return backup
|
|
122
|
+
i = 1
|
|
123
|
+
while True:
|
|
124
|
+
candidate = dest.with_name(f"{dest.name}.bak.{i}")
|
|
125
|
+
if not candidate.exists():
|
|
126
|
+
return candidate
|
|
127
|
+
i += 1
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _iter_bundled_files() -> List[Path]:
|
|
131
|
+
"""Every regular file under ``bundled/`` (skipping OS junk)."""
|
|
132
|
+
if not BUNDLED_DIR.is_dir():
|
|
133
|
+
return []
|
|
134
|
+
return sorted(
|
|
135
|
+
p for p in BUNDLED_DIR.rglob("*") if p.is_file() and p.name != ".DS_Store"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _load_manifest(config_dir: Path) -> Dict[str, str]:
|
|
140
|
+
manifest_path = config_dir / MANIFEST_NAME
|
|
141
|
+
try:
|
|
142
|
+
return json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
143
|
+
except (OSError, ValueError):
|
|
144
|
+
return {}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _save_manifest(config_dir: Path, manifest: Dict[str, str]) -> None:
|
|
148
|
+
manifest_path = config_dir / MANIFEST_NAME
|
|
149
|
+
_atomic_write_text(manifest_path, json.dumps(manifest, indent=2, sort_keys=True))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def read_installed_version(config_dir: Path) -> str | None:
|
|
153
|
+
"""Return the code-puppy version recorded on the last successful install."""
|
|
154
|
+
marker = config_dir / VERSION_MARKER_NAME
|
|
155
|
+
try:
|
|
156
|
+
return marker.read_text(encoding="utf-8").strip() or None
|
|
157
|
+
except OSError:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _write_version(config_dir: Path, version: str) -> None:
|
|
162
|
+
_atomic_write_text(config_dir / VERSION_MARKER_NAME, version)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def needs_install(config_dir: Path, current_version: str) -> bool:
|
|
166
|
+
"""True on a fresh install (no marker) or after a version bump."""
|
|
167
|
+
return read_installed_version(config_dir) != current_version
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _install_pass(config_dir: Path, current_version: str) -> InstallReport:
|
|
171
|
+
"""Copy the bundled payload into ``config_dir`` (the actual work).
|
|
172
|
+
|
|
173
|
+
Separated from :func:`install_bundled_commands` so the latter can wrap this
|
|
174
|
+
in a best-effort cross-process lock without deepening the copy logic.
|
|
175
|
+
"""
|
|
176
|
+
report = InstallReport()
|
|
177
|
+
manifest = _load_manifest(config_dir)
|
|
178
|
+
new_manifest: Dict[str, str] = {}
|
|
179
|
+
|
|
180
|
+
for src in _iter_bundled_files():
|
|
181
|
+
rel = src.relative_to(BUNDLED_DIR).as_posix()
|
|
182
|
+
dest = config_dir / rel
|
|
183
|
+
payload = src.read_bytes()
|
|
184
|
+
payload_hash = _sha256_bytes(payload)
|
|
185
|
+
|
|
186
|
+
if not dest.exists():
|
|
187
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
_write_bytes_preserving_mode(dest, payload, src)
|
|
189
|
+
report.installed.append(rel)
|
|
190
|
+
new_manifest[rel] = payload_hash
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
current_hash = _sha256(dest)
|
|
194
|
+
if current_hash == payload_hash:
|
|
195
|
+
# Already exactly our payload -- idempotent no-op. Claim it so we
|
|
196
|
+
# keep ownership across future bumps.
|
|
197
|
+
report.skipped.append(rel)
|
|
198
|
+
new_manifest[rel] = payload_hash
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
# Content differs from what we ship. Two cases:
|
|
202
|
+
# 1) ``rel not in manifest``: a pre-existing user-owned file with the same
|
|
203
|
+
# name — preserve it in place forever (never back up/overwrite/claim it),
|
|
204
|
+
# so a fresh install can't stomp the user's own global command.
|
|
205
|
+
# 2) ``rel in manifest``: ours. Untouched (hash matches) -> overwrite freely;
|
|
206
|
+
# otherwise the user hand-edited it -> keep theirs as a unique ``.bak``.
|
|
207
|
+
if rel not in manifest:
|
|
208
|
+
report.skipped.append(rel)
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
if manifest.get(rel) != current_hash:
|
|
212
|
+
backup = _unique_backup_path(dest)
|
|
213
|
+
shutil.copy2(dest, backup)
|
|
214
|
+
report.backed_up.append(backup.relative_to(config_dir).as_posix())
|
|
215
|
+
|
|
216
|
+
_write_bytes_preserving_mode(dest, payload, src)
|
|
217
|
+
report.updated.append(rel)
|
|
218
|
+
new_manifest[rel] = payload_hash
|
|
219
|
+
|
|
220
|
+
_save_manifest(config_dir, new_manifest)
|
|
221
|
+
_write_version(config_dir, current_version)
|
|
222
|
+
return report
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def install_bundled_commands(config_dir: Path, current_version: str) -> InstallReport:
|
|
226
|
+
"""Copy the bundled Flux payload into ``config_dir``.
|
|
227
|
+
|
|
228
|
+
``bundled/commands/...`` -> ``config_dir/commands/...``
|
|
229
|
+
``bundled/scripts/...`` -> ``config_dir/scripts/...``
|
|
230
|
+
|
|
231
|
+
Returns an :class:`InstallReport`. Writes the version marker + manifest only
|
|
232
|
+
after a clean pass so a crash mid-install re-runs next time.
|
|
233
|
+
|
|
234
|
+
Guarded by a best-effort exclusive ``flock`` on ``.flux_bootstrap.lock``: if
|
|
235
|
+
another instance already holds it (a concurrent first-launch install), we
|
|
236
|
+
skip and return an empty report rather than racing. The lock auto-releases
|
|
237
|
+
when the holding process dies, so a crashed install can't wedge future ones.
|
|
238
|
+
Where ``fcntl`` is unavailable (Windows), the pass runs unlocked -- the same
|
|
239
|
+
behavior as before this guard existed.
|
|
240
|
+
"""
|
|
241
|
+
config_dir = Path(config_dir)
|
|
242
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
|
|
244
|
+
if fcntl is None:
|
|
245
|
+
return _install_pass(config_dir, current_version)
|
|
246
|
+
|
|
247
|
+
lock_fd = os.open(config_dir / LOCK_NAME, os.O_CREAT | os.O_WRONLY, 0o644)
|
|
248
|
+
try:
|
|
249
|
+
try:
|
|
250
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
251
|
+
except OSError:
|
|
252
|
+
# Another process holds the lock -- it's installing; skip.
|
|
253
|
+
return InstallReport()
|
|
254
|
+
return _install_pass(config_dir, current_version)
|
|
255
|
+
finally:
|
|
256
|
+
os.close(lock_fd) # releases the flock
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Flux bootstrap plugin.
|
|
2
|
+
|
|
3
|
+
Installs the bundled Flux slash-command suite into ``~/.code_puppy`` on a fresh
|
|
4
|
+
install and after every code-puppy version bump, so ``/flux/...`` commands work
|
|
5
|
+
out of the box without the user copying anything by hand.
|
|
6
|
+
|
|
7
|
+
The heavy lifting (copy / backup / version-gating) lives in :mod:`installer`;
|
|
8
|
+
this module only wires it to the ``startup`` hook and makes sure a failure can
|
|
9
|
+
never crash the app.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from code_puppy.callbacks import register_callback
|
|
15
|
+
from code_puppy.config import CONFIG_DIR
|
|
16
|
+
from code_puppy.messaging import emit_info, emit_warning
|
|
17
|
+
|
|
18
|
+
from .installer import install_bundled_commands, needs_install
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _reload_command_cache() -> None:
|
|
22
|
+
"""Ask the customizable_commands loader to rescan its dirs. Never raises.
|
|
23
|
+
|
|
24
|
+
Imported lazily (not at module scope) so a refactor/rename of that plugin
|
|
25
|
+
can't break flux_bootstrap's own import, and so we don't create a hard
|
|
26
|
+
load-order dependency between the two plugins.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
from code_puppy_core_plugins.customizable_commands.register_callbacks import (
|
|
30
|
+
reload_commands,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
reload_commands()
|
|
34
|
+
except Exception as exc: # a stale cache is not worth crashing over
|
|
35
|
+
emit_warning(f"Flux installed but command cache reload failed: {exc}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _current_version() -> str:
|
|
39
|
+
try:
|
|
40
|
+
from code_puppy import __version__
|
|
41
|
+
|
|
42
|
+
return str(__version__)
|
|
43
|
+
except Exception:
|
|
44
|
+
return "0.0.0-dev"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _install_flux_commands() -> None:
|
|
48
|
+
"""Version-gated install of the bundled Flux command set. Never raises."""
|
|
49
|
+
try:
|
|
50
|
+
config_dir = Path(CONFIG_DIR)
|
|
51
|
+
version = _current_version()
|
|
52
|
+
|
|
53
|
+
if not needs_install(config_dir, version):
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
report = install_bundled_commands(config_dir, version)
|
|
57
|
+
if report.changed:
|
|
58
|
+
emit_info(f"Flux commands installed -> {config_dir} ({report.summary()})")
|
|
59
|
+
if report.backed_up:
|
|
60
|
+
emit_warning(
|
|
61
|
+
"Backed up locally-modified Flux files (see *.bak): "
|
|
62
|
+
+ ", ".join(report.backed_up)
|
|
63
|
+
)
|
|
64
|
+
# Command cache was populated at import time, before we wrote these files —
|
|
65
|
+
# rescan so /flux/... dispatches this session instead of after a restart.
|
|
66
|
+
_reload_command_cache()
|
|
67
|
+
except Exception as exc: # never let bootstrap break startup
|
|
68
|
+
emit_warning(f"Flux bootstrap skipped (install failed): {exc}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
register_callback("startup", _install_flux_commands)
|