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.
Files changed (313) hide show
  1. code_puppy_core_plugins/__init__.py +5 -0
  2. code_puppy_core_plugins/acp/README.md +266 -0
  3. code_puppy_core_plugins/acp/__init__.py +8 -0
  4. code_puppy_core_plugins/acp/agent.py +494 -0
  5. code_puppy_core_plugins/acp/bridge.py +309 -0
  6. code_puppy_core_plugins/acp/capabilities.py +73 -0
  7. code_puppy_core_plugins/acp/commands.py +116 -0
  8. code_puppy_core_plugins/acp/content.py +115 -0
  9. code_puppy_core_plugins/acp/io_delegation.py +231 -0
  10. code_puppy_core_plugins/acp/mcp_config.py +66 -0
  11. code_puppy_core_plugins/acp/permissions.py +159 -0
  12. code_puppy_core_plugins/acp/persistence.py +197 -0
  13. code_puppy_core_plugins/acp/register_callbacks.py +110 -0
  14. code_puppy_core_plugins/acp/replay.py +114 -0
  15. code_puppy_core_plugins/acp/session.py +329 -0
  16. code_puppy_core_plugins/acp/session_config.py +179 -0
  17. code_puppy_core_plugins/acp/state.py +135 -0
  18. code_puppy_core_plugins/agent_skills/__init__.py +22 -0
  19. code_puppy_core_plugins/agent_skills/config.py +208 -0
  20. code_puppy_core_plugins/agent_skills/discovery.py +337 -0
  21. code_puppy_core_plugins/agent_skills/downloader.py +392 -0
  22. code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
  23. code_puppy_core_plugins/agent_skills/installer.py +22 -0
  24. code_puppy_core_plugins/agent_skills/metadata.py +219 -0
  25. code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
  26. code_puppy_core_plugins/agent_skills/provider.py +52 -0
  27. code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
  28. code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
  29. code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
  30. code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
  31. code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
  32. code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
  33. code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
  34. code_puppy_core_plugins/aws_bedrock/config.py +101 -0
  35. code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
  36. code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
  37. code_puppy_core_plugins/azure_foundry/README.md +238 -0
  38. code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
  39. code_puppy_core_plugins/azure_foundry/config.py +126 -0
  40. code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
  41. code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
  42. code_puppy_core_plugins/azure_foundry/token.py +182 -0
  43. code_puppy_core_plugins/azure_foundry/utils.py +375 -0
  44. code_puppy_core_plugins/btw/__init__.py +1 -0
  45. code_puppy_core_plugins/btw/inline_view.py +135 -0
  46. code_puppy_core_plugins/btw/register_callbacks.py +106 -0
  47. code_puppy_core_plugins/btw/side_query.py +91 -0
  48. code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
  49. code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
  50. code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
  51. code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
  52. code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
  53. code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
  54. code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
  55. code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
  56. code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
  57. code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
  58. code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
  59. code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
  60. code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
  61. code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
  62. code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
  63. code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
  64. code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
  65. code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
  66. code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
  67. code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
  68. code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
  69. code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
  70. code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
  71. code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
  72. code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
  73. code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
  74. code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
  75. code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
  76. code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
  77. code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
  78. code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
  79. code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
  80. code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
  81. code_puppy_core_plugins/computer_use/README.md +109 -0
  82. code_puppy_core_plugins/computer_use/__init__.py +1 -0
  83. code_puppy_core_plugins/computer_use/accessibility.py +163 -0
  84. code_puppy_core_plugins/computer_use/activation.py +75 -0
  85. code_puppy_core_plugins/computer_use/backend.py +584 -0
  86. code_puppy_core_plugins/computer_use/backend_types.py +5 -0
  87. code_puppy_core_plugins/computer_use/capture.py +110 -0
  88. code_puppy_core_plugins/computer_use/commands.py +62 -0
  89. code_puppy_core_plugins/computer_use/geometry.py +81 -0
  90. code_puppy_core_plugins/computer_use/inline_image.py +56 -0
  91. code_puppy_core_plugins/computer_use/keycodes.py +47 -0
  92. code_puppy_core_plugins/computer_use/policy.py +144 -0
  93. code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
  94. code_puppy_core_plugins/computer_use/safety.py +18 -0
  95. code_puppy_core_plugins/computer_use/settle.py +64 -0
  96. code_puppy_core_plugins/computer_use/state.py +86 -0
  97. code_puppy_core_plugins/computer_use/tools.py +469 -0
  98. code_puppy_core_plugins/context_indicator/__init__.py +14 -0
  99. code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
  100. code_puppy_core_plugins/context_indicator/usage.py +40 -0
  101. code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
  102. code_puppy_core_plugins/copilot_auth/config.py +93 -0
  103. code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
  104. code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
  105. code_puppy_core_plugins/copilot_auth/utils.py +587 -0
  106. code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
  107. code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
  108. code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
  109. code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
  110. code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
  111. code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
  112. code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
  113. code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
  114. code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
  115. code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
  116. code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
  117. code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
  118. code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
  119. code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
  120. code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
  121. code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
  122. code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
  123. code_puppy_core_plugins/emoji_filter/config.py +26 -0
  124. code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
  125. code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
  126. code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
  127. code_puppy_core_plugins/example_custom_command/README.md +328 -0
  128. code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
  129. code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
  130. code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
  131. code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
  132. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
  133. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
  134. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
  135. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
  136. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
  137. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
  138. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
  139. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
  140. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
  141. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
  142. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
  143. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
  144. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
  145. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
  146. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
  147. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
  148. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
  149. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
  150. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
  151. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
  152. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
  153. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
  154. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
  155. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
  156. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
  157. code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
  158. code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
  159. code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
  160. code_puppy_core_plugins/force_push_guard/detector.py +96 -0
  161. code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
  162. code_puppy_core_plugins/fork/__init__.py +7 -0
  163. code_puppy_core_plugins/fork/register_callbacks.py +445 -0
  164. code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
  165. code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
  166. code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
  167. code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
  168. code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
  169. code_puppy_core_plugins/grok_oauth/config.py +49 -0
  170. code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
  171. code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
  172. code_puppy_core_plugins/grok_oauth/utils.py +195 -0
  173. code_puppy_core_plugins/herdr/README.md +168 -0
  174. code_puppy_core_plugins/herdr/__init__.py +9 -0
  175. code_puppy_core_plugins/herdr/client.py +339 -0
  176. code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
  177. code_puppy_core_plugins/herdr/reporter.py +212 -0
  178. code_puppy_core_plugins/herdr/smoke.py +143 -0
  179. code_puppy_core_plugins/herdr/sources.py +133 -0
  180. code_puppy_core_plugins/hook_creator/__init__.py +1 -0
  181. code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
  182. code_puppy_core_plugins/hook_manager/__init__.py +1 -0
  183. code_puppy_core_plugins/hook_manager/config.py +290 -0
  184. code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
  185. code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
  186. code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
  187. code_puppy_core_plugins/meta_oauth/config.py +55 -0
  188. code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
  189. code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
  190. code_puppy_core_plugins/meta_oauth/utils.py +252 -0
  191. code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
  192. code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
  193. code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
  194. code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
  195. code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
  196. code_puppy_core_plugins/no_tools/__init__.py +0 -0
  197. code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
  198. code_puppy_core_plugins/oauth_pasteback.py +110 -0
  199. code_puppy_core_plugins/oauth_puppy_html.py +194 -0
  200. code_puppy_core_plugins/obsidian_agent/README.md +52 -0
  201. code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
  202. code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
  203. code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
  204. code_puppy_core_plugins/ollama/__init__.py +1 -0
  205. code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
  206. code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
  207. code_puppy_core_plugins/ollama_setup/completer.py +38 -0
  208. code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
  209. code_puppy_core_plugins/plugin_list/__init__.py +0 -0
  210. code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
  211. code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
  212. code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
  213. code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
  214. code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
  215. code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
  216. code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
  217. code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
  218. code_puppy_core_plugins/pop_command/__init__.py +1 -0
  219. code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
  220. code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
  221. code_puppy_core_plugins/prompt_newline/config.py +21 -0
  222. code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
  223. code_puppy_core_plugins/prune/__init__.py +1 -0
  224. code_puppy_core_plugins/prune/prune_menu.py +308 -0
  225. code_puppy_core_plugins/prune/prune_model.py +517 -0
  226. code_puppy_core_plugins/prune/prune_render.py +354 -0
  227. code_puppy_core_plugins/prune/register_callbacks.py +388 -0
  228. code_puppy_core_plugins/puppy_kennel/README.md +203 -0
  229. code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
  230. code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
  231. code_puppy_core_plugins/puppy_kennel/config.py +36 -0
  232. code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
  233. code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
  234. code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
  235. code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
  236. code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
  237. code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
  238. code_puppy_core_plugins/puppy_kennel/state.py +68 -0
  239. code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
  240. code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
  241. code_puppy_core_plugins/puppy_spinner/README.md +93 -0
  242. code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
  243. code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
  244. code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
  245. code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
  246. code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
  247. code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
  248. code_puppy_core_plugins/quick_resume/__init__.py +0 -0
  249. code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
  250. code_puppy_core_plugins/review_pr/__init__.py +1 -0
  251. code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
  252. code_puppy_core_plugins/shell_safety/__init__.py +6 -0
  253. code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
  254. code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
  255. code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
  256. code_puppy_core_plugins/stack_dump/__init__.py +1 -0
  257. code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
  258. code_puppy_core_plugins/statusline/__init__.py +14 -0
  259. code_puppy_core_plugins/statusline/config.py +68 -0
  260. code_puppy_core_plugins/statusline/payload.py +138 -0
  261. code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
  262. code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
  263. code_puppy_core_plugins/statusline/runner.py +122 -0
  264. code_puppy_core_plugins/statusline/statusline_command.py +197 -0
  265. code_puppy_core_plugins/steer_queue/__init__.py +10 -0
  266. code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
  267. code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
  268. code_puppy_core_plugins/subagent_panel/README.md +102 -0
  269. code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
  270. code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
  271. code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
  272. code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
  273. code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
  274. code_puppy_core_plugins/subagent_panel/state.py +231 -0
  275. code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
  276. code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
  277. code_puppy_core_plugins/theme/README.md +86 -0
  278. code_puppy_core_plugins/theme/__init__.py +14 -0
  279. code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
  280. code_puppy_core_plugins/theme/content_styles.py +146 -0
  281. code_puppy_core_plugins/theme/osc_palette.py +178 -0
  282. code_puppy_core_plugins/theme/picker.py +358 -0
  283. code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
  284. code_puppy_core_plugins/theme/register_callbacks.py +348 -0
  285. code_puppy_core_plugins/theme/rich_themes.py +202 -0
  286. code_puppy_core_plugins/theme/themes.py +720 -0
  287. code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
  288. code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
  289. code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
  290. code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
  291. code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
  292. code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
  293. code_puppy_core_plugins/universal_constructor/models.py +138 -0
  294. code_puppy_core_plugins/universal_constructor/provider.py +62 -0
  295. code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
  296. code_puppy_core_plugins/universal_constructor/registry.py +302 -0
  297. code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
  298. code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
  299. code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
  300. code_puppy_core_plugins/wiggum/__init__.py +1 -0
  301. code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
  302. code_puppy_core_plugins/wiggum/judge.py +299 -0
  303. code_puppy_core_plugins/wiggum/judge_config.py +249 -0
  304. code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
  305. code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
  306. code_puppy_core_plugins/wiggum/state.py +89 -0
  307. code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
  308. code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
  309. code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
  310. code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
  311. code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
  312. code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
  313. 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)
@@ -0,0 +1,5 @@
1
+ """Force push guard plugin.
2
+
3
+ Blocks git force push commands and asks for explicit permission before
4
+ allowing them through. Prevents the classic "oops I nuked main" scenario.
5
+ """