mcli-framework 8.0.47__tar.gz → 8.0.49__tar.gz

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 (241) hide show
  1. {mcli_framework-8.0.47/src/mcli_framework.egg-info → mcli_framework-8.0.49}/PKG-INFO +2 -1
  2. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/pyproject.toml +2 -1
  3. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/main.py +9 -0
  4. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/sync_cmd.py +10 -9
  5. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/ipfs_sync.py +23 -17
  6. mcli_framework-8.0.49/src/mcli/workflow/ci/__init__.py +1 -0
  7. mcli_framework-8.0.49/src/mcli/workflow/ci/act_runner.py +61 -0
  8. mcli_framework-8.0.49/src/mcli/workflow/ci/ci.py +172 -0
  9. mcli_framework-8.0.49/src/mcli/workflow/ci/runner_status.py +26 -0
  10. mcli_framework-8.0.49/src/mcli/workflow/ci/workflow_transform.py +151 -0
  11. {mcli_framework-8.0.47 → mcli_framework-8.0.49/src/mcli_framework.egg-info}/PKG-INFO +2 -1
  12. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli_framework.egg-info/SOURCES.txt +5 -0
  13. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli_framework.egg-info/requires.txt +1 -0
  14. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/LICENSE +0 -0
  15. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/MANIFEST.in +0 -0
  16. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/README.md +0 -0
  17. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/llms-full.txt +0 -0
  18. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/llms.txt +0 -0
  19. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/mcli_rust/Cargo.toml +0 -0
  20. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/mcli_rust/src/command_parser.rs +0 -0
  21. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/mcli_rust/src/file_watcher.rs +0 -0
  22. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/mcli_rust/src/lib.rs +0 -0
  23. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/mcli_rust/src/process_manager.rs +0 -0
  24. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/mcli_rust/src/tfidf.rs +0 -0
  25. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/setup.cfg +0 -0
  26. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/__init__.py +0 -0
  27. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/__init__.py +0 -0
  28. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/commands_cmd.py +0 -0
  29. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/completion_helpers.py +0 -0
  30. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/context_cmd.py +0 -0
  31. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/create_cmd.py +0 -0
  32. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/delete_cmd.py +0 -0
  33. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/edit_cmd.py +0 -0
  34. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/import_cmd.py +0 -0
  35. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/init_cmd.py +0 -0
  36. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/list_cmd.py +0 -0
  37. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/migrate_cmd.py +0 -0
  38. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/model/__init__.py +0 -0
  39. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/model/model.py +0 -0
  40. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/model_cmd.py +0 -0
  41. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/mv_cmd.py +0 -0
  42. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/new_cmd.py +0 -0
  43. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/remove_cmd.py +0 -0
  44. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/rm_cmd.py +0 -0
  45. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/search_cmd.py +0 -0
  46. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/services_cmd.py +0 -0
  47. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/setup_cmd.py +0 -0
  48. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/source_sync_cmd.py +0 -0
  49. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/video/__init__.py +0 -0
  50. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/app/video/video.py +0 -0
  51. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/config.toml +0 -0
  52. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/__init__.py +0 -0
  53. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/api/__init__.py +0 -0
  54. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/api/api.py +0 -0
  55. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/api/daemon_client.py +0 -0
  56. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/api/daemon_client_local.py +0 -0
  57. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/api/daemon_decorator.py +0 -0
  58. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/api/mcli_decorators.py +0 -0
  59. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/auth/__init__.py +0 -0
  60. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/auth/auth.py +0 -0
  61. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/auth/aws_manager.py +0 -0
  62. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/auth/azure_manager.py +0 -0
  63. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/auth/credential_manager.py +0 -0
  64. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/auth/gcp_manager.py +0 -0
  65. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/auth/key_manager.py +0 -0
  66. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/auth/mcli_manager.py +0 -0
  67. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/auth/token_manager.py +0 -0
  68. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/auth/token_util.py +0 -0
  69. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/config/__init__.py +0 -0
  70. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/config/config.py +0 -0
  71. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/config/settings.py +0 -0
  72. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/constants/__init__.py +0 -0
  73. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/constants/commands.py +0 -0
  74. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/constants/defaults.py +0 -0
  75. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/constants/env.py +0 -0
  76. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/constants/messages.py +0 -0
  77. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/constants/paths.py +0 -0
  78. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/constants/scripts.py +0 -0
  79. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/constants/storage.py +0 -0
  80. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/custom_commands.py +0 -0
  81. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/discovery/__init__.py +0 -0
  82. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/discovery/command_discovery.py +0 -0
  83. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/erd/__init__.py +0 -0
  84. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/erd/erd.py +0 -0
  85. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/erd/generate_graph.py +0 -0
  86. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/errors.py +0 -0
  87. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/feature_detection.py +0 -0
  88. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/files/__init__.py +0 -0
  89. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/files/files.py +0 -0
  90. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/folder_workflows.py +0 -0
  91. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/fs/__init__.py +0 -0
  92. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/fs/fs.py +0 -0
  93. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/ipfs_utils.py +0 -0
  94. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/ipns_manager.py +0 -0
  95. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/lib.py +0 -0
  96. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/logger/__init__.py +0 -0
  97. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/logger/correlation.py +0 -0
  98. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/logger/logger.py +0 -0
  99. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/logger/structured.py +0 -0
  100. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/optional_deps.py +0 -0
  101. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/paths.py +0 -0
  102. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/performance/__init__.py +0 -0
  103. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/performance/optimizer.py +0 -0
  104. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/performance/rust_bridge.py +0 -0
  105. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/performance/uvloop_config.py +0 -0
  106. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/pickles/__init__.py +0 -0
  107. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/pickles/pickles.py +0 -0
  108. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/pyenv/__init__.py +0 -0
  109. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/pyenv/deps.py +0 -0
  110. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/pyenv/manager.py +0 -0
  111. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/pyenv/venv.py +0 -0
  112. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/script_loader.py +0 -0
  113. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/script_sync.py +0 -0
  114. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/script_watcher.py +0 -0
  115. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/search/cached_vectorizer.py +0 -0
  116. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/secrets/__init__.py +0 -0
  117. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/secrets/commands.py +0 -0
  118. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/secrets/manager.py +0 -0
  119. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/secrets/repl.py +0 -0
  120. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/secrets/store.py +0 -0
  121. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/services/__init__.py +0 -0
  122. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/services/config.py +0 -0
  123. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/services/data_pipeline.py +0 -0
  124. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/services/health.py +0 -0
  125. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/services/lsh_client.py +0 -0
  126. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/services/manager.py +0 -0
  127. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/services/redis_service.py +0 -0
  128. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/services/registry.py +0 -0
  129. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/services/state.py +0 -0
  130. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/services/supervisor.py +0 -0
  131. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/shell/__init__.py +0 -0
  132. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/shell/exceptions.py +0 -0
  133. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/shell/shell.py +0 -0
  134. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/sync_key_store.py +0 -0
  135. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/templates/__init__.py +0 -0
  136. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/templates/command_templates.py +0 -0
  137. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/toml/__init__.py +0 -0
  138. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/toml/toml.py +0 -0
  139. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/types.py +0 -0
  140. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/ui/styling.py +0 -0
  141. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/ui/visual_effects.py +0 -0
  142. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/watcher/__init__.py +0 -0
  143. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/watcher/watcher.py +0 -0
  144. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/workflow_models.py +0 -0
  145. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/lib/workspace_registry.py +0 -0
  146. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/mygroup/__init__.py +0 -0
  147. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/mygroup/test_cmd.py +0 -0
  148. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/public/__init__.py +0 -0
  149. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/public/commands/__init__.py +0 -0
  150. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/public/oi/oi.py +0 -0
  151. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/public/public.py +0 -0
  152. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/__init__.py +0 -0
  153. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/completion_cmd.py +0 -0
  154. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/env_cmd.py +0 -0
  155. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/health_cmd.py +0 -0
  156. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/ipfs_cmd.py +0 -0
  157. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/logs_cmd.py +0 -0
  158. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/migrate_cmd.py +0 -0
  159. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/release_notes_cmd.py +0 -0
  160. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/self_cmd.py +0 -0
  161. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/store_cmd.py +0 -0
  162. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/test_cmd.py +0 -0
  163. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/self/workflows_cmd.py +0 -0
  164. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/storage/__init__.py +0 -0
  165. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/storage/backends/__init__.py +0 -0
  166. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/storage/backends/ipfs_backend.py +0 -0
  167. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/storage/base.py +0 -0
  168. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/storage/cache.py +0 -0
  169. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/storage/encryption.py +0 -0
  170. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/storage/factory.py +0 -0
  171. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/storage/registry.py +0 -0
  172. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/storage/storacha_cli.py +0 -0
  173. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/__init__.py +0 -0
  174. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/__init__.py +0 -0
  175. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/async_command_database.py +0 -0
  176. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/async_process_manager.py +0 -0
  177. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/client.py +0 -0
  178. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/daemon.py +0 -0
  179. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/daemon_api.py +0 -0
  180. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/enhanced_daemon.py +0 -0
  181. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/process_cli.py +0 -0
  182. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/process_manager.py +0 -0
  183. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/test_daemon.py +0 -0
  184. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/doc_convert.py +0 -0
  185. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/docker/__init__.py +0 -0
  186. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/docker/docker.py +0 -0
  187. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/file/__init__.py +0 -0
  188. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/gcloud/__init__.py +0 -0
  189. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/gcloud/config.toml +0 -0
  190. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/gcloud/gcloud.py +0 -0
  191. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/git_commit/__init__.py +0 -0
  192. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/git_commit/ai_service.py +0 -0
  193. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/interview/__init__.py +0 -0
  194. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/lsh_integration.py +0 -0
  195. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/client.py +0 -0
  196. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/download_and_run_efficient_models.py +0 -0
  197. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/lightweight_embedder.py +0 -0
  198. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/lightweight_model_server.py +0 -0
  199. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/lightweight_test.py +0 -0
  200. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/model_service.py +0 -0
  201. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/ollama_efficient_runner.py +0 -0
  202. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/openai_adapter.py +0 -0
  203. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/pdf_processor.py +0 -0
  204. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/test_efficient_runner.py +0 -0
  205. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/test_example.py +0 -0
  206. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/test_integration.py +0 -0
  207. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/test_new_features.py +0 -0
  208. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/__init__.py +0 -0
  209. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/command_loader.py +0 -0
  210. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/converter.py +0 -0
  211. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/executor.py +0 -0
  212. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/notebook_cmd.py +0 -0
  213. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/schema.py +0 -0
  214. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/validator.py +0 -0
  215. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/openai/openai.py +0 -0
  216. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/registry/__init__.py +0 -0
  217. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/registry/registry.py +0 -0
  218. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/repo/__init__.py +0 -0
  219. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/repo/repo.py +0 -0
  220. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/__init__.py +0 -0
  221. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/cron_parser.py +0 -0
  222. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/job.py +0 -0
  223. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/models.py +0 -0
  224. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/monitor.py +0 -0
  225. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/persistence.py +0 -0
  226. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/scheduler.py +0 -0
  227. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/validation.py +0 -0
  228. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/search/__init__.py +0 -0
  229. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/secrets/__init__.py +0 -0
  230. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/secrets/secrets_cmd.py +0 -0
  231. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/storage/__init__.py +0 -0
  232. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/storage/storage_cmd.py +0 -0
  233. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/sync/__init__.py +0 -0
  234. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/sync/test_cmd.py +0 -0
  235. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/videos/__init__.py +0 -0
  236. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/wakatime/__init__.py +0 -0
  237. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/wakatime/wakatime.py +0 -0
  238. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli/workflow/workflow.py +0 -0
  239. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli_framework.egg-info/dependency_links.txt +0 -0
  240. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli_framework.egg-info/entry_points.txt +0 -0
  241. {mcli_framework-8.0.47 → mcli_framework-8.0.49}/src/mcli_framework.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcli-framework
3
- Version: 8.0.47
3
+ Version: 8.0.49
4
4
  Summary: Portable workflow framework - transform any script into a versioned, schedulable command. Store in ~/.mcli/workflows/, version with lockfile, run as daemon or cron job.
5
5
  Author-email: Luis Fernandez de la Vara <luis@lefv.io>
6
6
  Maintainer-email: Luis Fernandez de la Vara <luis@lefv.io>
@@ -37,6 +37,7 @@ Requires-Python: >=3.10
37
37
  Description-Content-Type: text/markdown
38
38
  License-File: LICENSE
39
39
  Requires-Dist: click<9.0.0,>=8.1.7
40
+ Requires-Dist: ruamel.yaml<0.19,>=0.18
40
41
  Requires-Dist: rich<15.0.0,>=14.0.0
41
42
  Requires-Dist: requests<3.0.0,>=2.31.0
42
43
  Requires-Dist: tomli<3.0.0,>=2.2.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcli-framework"
3
- version = "8.0.47"
3
+ version = "8.0.49"
4
4
  description = "Portable workflow framework - transform any script into a versioned, schedulable command. Store in ~/.mcli/workflows/, version with lockfile, run as daemon or cron job."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -37,6 +37,7 @@ classifiers = [
37
37
  dependencies = [
38
38
  # Core CLI dependencies
39
39
  "click>=8.1.7,<9.0.0",
40
+ "ruamel.yaml>=0.18,<0.19",
40
41
  "rich>=14.0.0,<15.0.0",
41
42
  "requests>=2.31.0,<3.0.0",
42
43
  "tomli>=2.2.1,<3.0.0",
@@ -447,6 +447,15 @@ def _add_lazy_commands(app: click.Group):
447
447
  except ImportError as e:
448
448
  logger.debug(f"Could not load sync group: {e}")
449
449
 
450
+ # mcli ci - act-first CI gate + hosted-trigger migration
451
+ try:
452
+ from mcli.workflow.ci.ci import ci
453
+
454
+ app.add_command(ci, name="ci")
455
+ logger.debug("Added ci group")
456
+ except ImportError as e:
457
+ logger.debug(f"Could not load ci group: {e}")
458
+
450
459
  # mcli setup - Onboarding wizard for new users
451
460
  try:
452
461
  from mcli.app.setup_cmd import setup
@@ -495,20 +495,17 @@ def sync_pull_command(
495
495
 
496
496
  ipfs = IPFSSync()
497
497
 
498
- if cid:
499
- # Explicit CID — just retrieve it
500
- info(SyncMessages.RETRIEVING_FROM_IPFS.format(cid=cid))
501
- data = ipfs.pull(cid, verify=not no_verify)
502
- else:
503
- # No CID — resolve via IPNS
498
+ if not cid:
499
+ # No CID — resolve via IPNS, then fall through to the explicit-CID flow
500
+ # so that --workflows-dir extraction also works for IPNS-resolved pulls.
504
501
  if not ensure_daemon_running():
505
502
  error(SyncMessages.DAEMON_START_FAILED)
506
503
  return
507
504
 
508
505
  info(SyncMessages.IPNS_RESOLVING)
509
- data = ipfs.pull_latest(scope="global", repo_name=repo)
506
+ cid = ipfs.resolve_latest_cid(scope="global", repo_name=repo)
510
507
 
511
- if data is None:
508
+ if not cid:
512
509
  from mcli.lib.ipns_manager import get_sync_key
513
510
 
514
511
  if not get_sync_key():
@@ -520,6 +517,10 @@ def sync_pull_command(
520
517
  console.print(SyncMessages.IPNS_PULL_HINT)
521
518
  return
522
519
 
520
+ # Explicit or just-resolved CID — retrieve it
521
+ info(SyncMessages.RETRIEVING_FROM_IPFS.format(cid=cid))
522
+ data = ipfs.pull(cid, verify=not no_verify)
523
+
523
524
  if data:
524
525
  success(SyncMessages.RETRIEVED_FROM_IPFS)
525
526
 
@@ -543,7 +544,7 @@ def sync_pull_command(
543
544
  console.print(SyncMessages.VERSION_LABEL.format(version=data["version"]))
544
545
 
545
546
  # Optional: reconstruct script files from per-script CIDs
546
- if workflows_dir and cid:
547
+ if workflows_dir:
547
548
  try:
548
549
  written = ipfs.pull_workflows(cid, workflows_dir, verify=not no_verify)
549
550
  except ValueError as exc:
@@ -541,45 +541,51 @@ class IPFSSync:
541
541
 
542
542
  return written
543
543
 
544
- def pull_latest(
544
+ def resolve_latest_cid(
545
545
  self,
546
546
  scope: str = "global",
547
547
  repo_name: Optional[str] = None,
548
- verify: bool = True,
549
- ) -> Optional[dict]:
550
- """Pull the latest workflow state by resolving via IPNS.
551
-
552
- Requires MCLI_SYNC_KEY to be set. Uses deterministic IPNS key
553
- derivation to find the latest CID without explicit CID sharing.
548
+ ) -> Optional[str]:
549
+ """Resolve the deterministic IPNS name to a current CID.
554
550
 
555
- Args:
556
- scope: Sync scope (e.g. "global" or "local")
557
- repo_name: Override auto-detected repo name (for cross-repo pull)
558
- verify: Whether to verify hash integrity
559
-
560
- Returns:
561
- Command data if successful, None otherwise
551
+ Returns the CID string on success, ``None`` if the sync key is
552
+ unset, the key cannot be imported into Kubo, or IPNS resolution
553
+ fails. Surfaces the CID so CLI callers can hand it to
554
+ ``pull_workflows`` for script extraction.
562
555
  """
563
556
  sync_key = get_sync_key()
564
557
  if not sync_key:
565
- logger.warning("MCLI_SYNC_KEY not set — cannot resolve via IPNS")
558
+ logger.warning("Sync key not configured — cannot resolve via IPNS")
566
559
  return None
567
560
 
568
561
  repo = repo_name or get_repo_name()
569
562
  key_info = derive_key_info(sync_key, repo, scope)
570
563
 
571
- # Ensure key is in Kubo so we can resolve
572
564
  ipns_name = ensure_key_imported(key_info)
573
565
  if not ipns_name:
574
566
  logger.error("Failed to import IPNS key into Kubo")
575
567
  return None
576
568
 
577
- # Resolve IPNS to CID
578
569
  cid = resolve_ipns(ipns_name)
579
570
  if not cid:
580
571
  logger.error("IPNS resolution failed — no workflows published yet?")
581
572
  return None
573
+ return cid
574
+
575
+ def pull_latest(
576
+ self,
577
+ scope: str = "global",
578
+ repo_name: Optional[str] = None,
579
+ verify: bool = True,
580
+ ) -> Optional[dict]:
581
+ """Pull the latest workflow state by resolving via IPNS.
582
582
 
583
+ Requires the sync key to be configured (env var or store).
584
+ Returns the command data dict on success, ``None`` otherwise.
585
+ """
586
+ cid = self.resolve_latest_cid(scope=scope, repo_name=repo_name)
587
+ if not cid:
588
+ return None
583
589
  return self.pull(cid, verify=verify)
584
590
 
585
591
  def get_history(self, limit: int = 10) -> list[dict]:
@@ -0,0 +1 @@
1
+ """act-first CI tooling: local act gate + hosted-trigger stripping for private repos."""
@@ -0,0 +1,61 @@
1
+ """Run `act` locally and classify the outcome for the PR gate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ from enum import Enum
8
+ from pathlib import Path
9
+
10
+
11
+ class PreflightResult(Enum):
12
+ PASS = "pass"
13
+ FAIL = "fail"
14
+ UNREACHABLE = "unreachable"
15
+
16
+
17
+ def act_available() -> bool:
18
+ return shutil.which("act") is not None
19
+
20
+
21
+ def docker_running() -> bool:
22
+ try:
23
+ proc = subprocess.run(["docker", "info"], capture_output=True, timeout=30)
24
+ return proc.returncode == 0
25
+ except (FileNotFoundError, subprocess.TimeoutExpired):
26
+ return False
27
+
28
+
29
+ def probe() -> bool:
30
+ """Can act actually run here? Needs the binary, a live docker daemon, and `act -l`."""
31
+ if not act_available() or not docker_running():
32
+ return False
33
+ try:
34
+ proc = subprocess.run(["act", "-l"], capture_output=True, text=True, timeout=60)
35
+ except (FileNotFoundError, subprocess.TimeoutExpired):
36
+ return False
37
+ return proc.returncode == 0
38
+
39
+
40
+ def build_act_command(event: str) -> list[str]:
41
+ cmd = ["act", event]
42
+ if Path(".secrets").exists():
43
+ cmd += ["--secret-file", ".secrets"]
44
+ return cmd
45
+
46
+
47
+ def run_act(event: str = "pull_request") -> PreflightResult:
48
+ """Run act for `event`. PASS on exit 0, else FAIL. (Probe gates UNREACHABLE upstream.)"""
49
+ proc = subprocess.run(build_act_command(event))
50
+ return PreflightResult.PASS if proc.returncode == 0 else PreflightResult.FAIL
51
+
52
+
53
+ def preflight(repo_slug: str, event: str = "pull_request") -> PreflightResult:
54
+ """Primary gate. PASS/FAIL if act can run; UNREACHABLE if act can't start here.
55
+
56
+ `repo_slug` is accepted for symmetry and future use; the runner fallback is
57
+ orchestrated by the CLI layer based on runner_status.has_online_runner.
58
+ """
59
+ if not probe():
60
+ return PreflightResult.UNREACHABLE
61
+ return run_act(event)
@@ -0,0 +1,172 @@
1
+ """`mcli ci` — act-first CI gate and hosted-trigger migration for private repos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import stat
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+ from mcli.workflow.ci.act_runner import PreflightResult, act_available, docker_running
13
+ from mcli.workflow.ci.act_runner import preflight as preflight_fn
14
+ from mcli.workflow.ci.runner_status import has_online_runner
15
+ from mcli.workflow.ci.workflow_transform import transform_file, write_self_hosted_workflow
16
+
17
+ _GITHUB_REMOTE_RE = re.compile(
18
+ r"(?:git@github\.com:|https://github\.com/)([^/]+/[^/]+?)(?:\.git)?/?$"
19
+ )
20
+
21
+
22
+ def current_repo_slug() -> str | None:
23
+ """owner/name from a github.com origin remote, or None (non-GitHub or no remote)."""
24
+ try:
25
+ url = subprocess.run(
26
+ ["git", "remote", "get-url", "origin"],
27
+ capture_output=True,
28
+ text=True,
29
+ timeout=10,
30
+ ).stdout.strip()
31
+ except (FileNotFoundError, subprocess.TimeoutExpired):
32
+ return None
33
+ if not url:
34
+ return None
35
+ match = _GITHUB_REMOTE_RE.match(url)
36
+ return match.group(1) if match else None
37
+
38
+
39
+ def detect_test_command() -> str:
40
+ """Best-effort test command for the self-hosted fallback workflow."""
41
+ makefile = Path("Makefile")
42
+ if makefile.exists():
43
+ txt = makefile.read_text()
44
+ if "\ntest:" in txt or txt.startswith("test:"):
45
+ return "make test"
46
+ if Path("pyproject.toml").exists() or Path("pytest.ini").exists():
47
+ return "uv run pytest -v || pytest -v"
48
+ if Path("package.json").exists():
49
+ return "npm test"
50
+ if Path("mix.exs").exists():
51
+ return "mix test"
52
+ return "echo 'TODO: set test command' && exit 1"
53
+
54
+
55
+ def workflows_dir() -> Path:
56
+ return Path(".github") / "workflows"
57
+
58
+
59
+ @click.group()
60
+ def ci():
61
+ """act-first CI: local act gate + stop billed hosted runners on private repos."""
62
+
63
+
64
+ @ci.command()
65
+ @click.option("--dry-run", is_flag=True, help="Show what would change without writing.")
66
+ def migrate(dry_run):
67
+ """Strip hosted triggers from this repo's workflows + add self-hosted fallback."""
68
+ wfdir = workflows_dir()
69
+ if not wfdir.exists():
70
+ click.echo("No .github/workflows directory; nothing to migrate.")
71
+ return
72
+ slug = current_repo_slug()
73
+ has_runner = has_online_runner(slug) if slug else False
74
+ test_cmd = detect_test_command()
75
+
76
+ files = sorted(p for p in wfdir.glob("*.y*ml") if p.name != "self-hosted-ci.yml")
77
+ if dry_run:
78
+ from mcli.workflow.ci.workflow_transform import MARKER, _yaml, workflow_has_hosted_job
79
+
80
+ for f in files:
81
+ text = f.read_text()
82
+ if MARKER in text:
83
+ click.echo(f" skip (already migrated): {f.name}")
84
+ continue
85
+ hosted = workflow_has_hosted_job(_yaml().load(text))
86
+ click.echo(f" {'STRIP' if hosted else 'keep '}: {f.name}")
87
+ click.echo(f" fallback self-hosted-ci.yml (pull_request={has_runner}, test='{test_cmd}')")
88
+ return
89
+
90
+ changed = [f.name for f in files if transform_file(f)]
91
+ created = write_self_hosted_workflow(wfdir, test_cmd, with_pull_request=has_runner)
92
+ for name in changed:
93
+ click.echo(f" stripped: {name}")
94
+ if created:
95
+ click.echo(f" created: self-hosted-ci.yml (pull_request={has_runner})")
96
+ click.echo(f"Done. {len(changed)} workflow(s) migrated.")
97
+
98
+
99
+ @ci.command()
100
+ @click.option("--event", default="pull_request", show_default=True, help="act event to simulate.")
101
+ def preflight(event):
102
+ """Run act as the PR gate. Exit 0=pass, 1=fail, 2=cannot validate, 3=use runner."""
103
+ slug = current_repo_slug()
104
+ result = preflight_fn(slug, event)
105
+ if result == PreflightResult.PASS:
106
+ click.echo("✅ act passed — OK to open PR.")
107
+ raise SystemExit(0)
108
+ if result == PreflightResult.FAIL:
109
+ click.echo("❌ act failed — fix before opening PR.")
110
+ raise SystemExit(1)
111
+ # UNREACHABLE
112
+ if slug and has_online_runner(slug):
113
+ click.echo(
114
+ "⚠️ act unreachable here; an online runner exists — "
115
+ "push and let the self-hosted runner validate."
116
+ )
117
+ raise SystemExit(3)
118
+ click.echo("⚠️ act unreachable and no online runner — cannot validate this PR.")
119
+ raise SystemExit(2)
120
+
121
+
122
+ @ci.command()
123
+ def pr():
124
+ """preflight, then `gh pr create --fill --base main` if it passed."""
125
+ slug = current_repo_slug()
126
+ result = preflight_fn(slug)
127
+ if result == PreflightResult.PASS:
128
+ # check=False on purpose: let gh/git stream their own errors to the terminal
129
+ # rather than raising CalledProcessError and hiding their output.
130
+ subprocess.run(["gh", "pr", "create", "--fill", "--base", "main"], check=False)
131
+ return
132
+ if result == PreflightResult.FAIL:
133
+ click.echo("act failed; not opening PR.")
134
+ raise SystemExit(1)
135
+ if slug and has_online_runner(slug):
136
+ click.echo("act unreachable; pushing so the runner can validate.")
137
+ subprocess.run(["git", "push", "-u", "origin", "HEAD"], check=False)
138
+ subprocess.run(["gh", "pr", "create", "--fill", "--base", "main"], check=False)
139
+ return
140
+ click.echo("act unreachable and no runner; refusing to open an unvalidated PR.")
141
+ raise SystemExit(2)
142
+
143
+
144
+ PRE_PUSH_HOOK = """#!/usr/bin/env bash
145
+ # mcli-ci pre-push gate: validate with act before pushing.
146
+ exec mcli ci preflight
147
+ """
148
+
149
+
150
+ @ci.command()
151
+ def doctor():
152
+ """Show act/docker/runner status for this repo."""
153
+ click.echo(f"act installed: {act_available()}")
154
+ click.echo(f"docker running: {docker_running()}")
155
+ slug = current_repo_slug()
156
+ click.echo(f"repo: {slug or '(no origin)'}")
157
+ if slug:
158
+ click.echo(f"online runner: {has_online_runner(slug)}")
159
+
160
+
161
+ @ci.command(name="install-hook")
162
+ def install_hook():
163
+ """Install an opt-in pre-push hook that runs `mcli ci preflight`."""
164
+ hooks = Path(".git") / "hooks"
165
+ if not hooks.exists():
166
+ click.echo("Not a git repo (.git/hooks missing).")
167
+ raise SystemExit(1)
168
+ hook = hooks / "pre-push"
169
+ hook.write_text(PRE_PUSH_HOOK)
170
+ mode = hook.stat().st_mode
171
+ hook.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
172
+ click.echo(f"Installed pre-push hook at {hook}")
@@ -0,0 +1,26 @@
1
+ """Query GitHub for self-hosted runner availability via the gh CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+
8
+
9
+ def has_online_runner(repo_slug: str) -> bool:
10
+ """True if `repo_slug` (owner/name) has at least one online self-hosted runner."""
11
+ try:
12
+ proc = subprocess.run(
13
+ ["gh", "api", f"repos/{repo_slug}/actions/runners"],
14
+ capture_output=True,
15
+ text=True,
16
+ timeout=30,
17
+ )
18
+ except (FileNotFoundError, subprocess.TimeoutExpired):
19
+ return False
20
+ if proc.returncode != 0:
21
+ return False
22
+ try:
23
+ data = json.loads(proc.stdout or "{}")
24
+ except json.JSONDecodeError:
25
+ return False
26
+ return any(r.get("status") == "online" for r in data.get("runners", []))
@@ -0,0 +1,151 @@
1
+ """Transform GitHub Actions workflows for act-first CI on private repos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from io import StringIO
6
+ from pathlib import Path
7
+
8
+ from ruamel.yaml import YAML
9
+
10
+ MARKER = "mcli-ci: hosted-triggers-stripped"
11
+ SELF_HOSTED_FILENAME = "self-hosted-ci.yml"
12
+ HOSTED_PREFIXES = ("ubuntu", "macos", "windows")
13
+
14
+
15
+ def is_hosted_label(label: str) -> bool:
16
+ """True if a runs-on label names a GitHub-hosted runner image."""
17
+ return str(label).lower().startswith(HOSTED_PREFIXES)
18
+
19
+
20
+ def runs_on_is_hosted(runs_on) -> bool:
21
+ """Classify a job's runs-on. Conservative: unknown expressions count as hosted."""
22
+ if runs_on is None:
23
+ return False
24
+ if isinstance(runs_on, (list, tuple)):
25
+ labels = [str(x) for x in runs_on]
26
+ if any("self-hosted" in lbl for lbl in labels):
27
+ return False
28
+ return any(is_hosted_label(lbl) for lbl in labels)
29
+ text = str(runs_on)
30
+ if "self-hosted" in text:
31
+ return False
32
+ if "${{" in text:
33
+ return True # unknown matrix/expression -> assume hosted to stop cost
34
+ return is_hosted_label(text)
35
+
36
+
37
+ # NOTE: ruamel.yaml round-trip (YAML(), typ='rt') is the SAFE, correct loader here.
38
+ # It does NOT execute `!!python/object` tags like PyYAML's yaml.load(). Do not
39
+ # "fix" this to PyYAML safe_load — that would strip comments/formatting and break
40
+ # round-tripping. Round-trip preservation is a hard requirement of this transform.
41
+ def _yaml() -> YAML:
42
+ y = YAML()
43
+ y.version = (1, 2) # critical: keeps bare `on:` a string, not boolean True
44
+ y.preserve_quotes = True
45
+ y.width = 4096 # avoid reflowing long lines
46
+ y.indent(mapping=2, sequence=4, offset=2)
47
+ return y
48
+
49
+
50
+ def workflow_has_hosted_job(doc) -> bool:
51
+ """True if any job in the parsed workflow targets a GitHub-hosted runner."""
52
+ if not isinstance(doc, dict):
53
+ return False
54
+ jobs = doc.get("jobs") or {}
55
+ for job in jobs.values():
56
+ if isinstance(job, dict) and runs_on_is_hosted(job.get("runs-on")):
57
+ return True
58
+ return False
59
+
60
+
61
+ def strip_hosted_triggers(doc) -> bool:
62
+ """Remove push/pull_request from `on:`, ensure workflow_dispatch. Returns changed."""
63
+ on = doc.get("on")
64
+ if on is None:
65
+ return False
66
+ changed = False
67
+ if isinstance(on, dict):
68
+ for key in ("push", "pull_request"):
69
+ if key in on:
70
+ del on[key]
71
+ changed = True
72
+ if "workflow_dispatch" not in on:
73
+ on["workflow_dispatch"] = None
74
+ changed = True
75
+ elif isinstance(on, list):
76
+ for key in ("push", "pull_request"):
77
+ while key in on:
78
+ on.remove(key)
79
+ changed = True
80
+ if "workflow_dispatch" not in on:
81
+ on.append("workflow_dispatch")
82
+ changed = True
83
+ elif isinstance(on, str):
84
+ if on in ("push", "pull_request"):
85
+ doc["on"] = "workflow_dispatch"
86
+ changed = True
87
+ return changed
88
+
89
+
90
+ def transform_file(path: Path) -> bool:
91
+ """Strip hosted triggers in-place if the workflow has a hosted job. Idempotent."""
92
+ path = Path(path)
93
+ text = path.read_text()
94
+ if MARKER in text:
95
+ return False
96
+ yaml = _yaml()
97
+ doc = yaml.load(text)
98
+ if not workflow_has_hosted_job(doc):
99
+ return False
100
+ # `on:` is workflow-level. If a workflow mixes hosted and self-hosted jobs,
101
+ # stripping push/pull_request also removes the self-hosted job's auto-trigger.
102
+ # Acceptable here: the separate self-hosted-ci.yml provides the runner PR path
103
+ # when a runner exists. Re-add triggers by hand if a single workflow needs both.
104
+ strip_hosted_triggers(doc)
105
+ doc.yaml_set_start_comment(f"{MARKER}\n")
106
+ # Pinning version=(1,2) at load keeps `on` a string key (not YAML 1.1 bool True).
107
+ # Reset it before dumping so ruamel does not prepend a `%YAML 1.2` / `---` directive
108
+ # to the workflow file (the key is already a plain string in the loaded document).
109
+ yaml.version = None
110
+ buf = StringIO()
111
+ yaml.dump(doc, buf)
112
+ path.write_text(buf.getvalue())
113
+ return True
114
+
115
+
116
+ def render_self_hosted_workflow(test_command: str, with_pull_request: bool) -> str:
117
+ """Render the dormant self-hosted fallback workflow as YAML text."""
118
+ triggers = " workflow_dispatch:\n"
119
+ if with_pull_request:
120
+ triggers += " pull_request:\n"
121
+ ref = "${{ github.ref }}"
122
+ return (
123
+ f"# {MARKER}\n"
124
+ "name: self-hosted-ci\n"
125
+ "on:\n"
126
+ f"{triggers}"
127
+ "concurrency:\n"
128
+ f" group: self-hosted-ci-{ref}\n"
129
+ " cancel-in-progress: true\n"
130
+ "jobs:\n"
131
+ " test:\n"
132
+ " runs-on: [self-hosted, Linux, X64]\n"
133
+ " timeout-minutes: 30\n"
134
+ " steps:\n"
135
+ " - uses: actions/checkout@v4\n"
136
+ " - name: Run tests\n"
137
+ f" run: {test_command}\n"
138
+ )
139
+
140
+
141
+ def write_self_hosted_workflow(
142
+ workflows_dir: Path, test_command: str, with_pull_request: bool
143
+ ) -> bool:
144
+ """Write self-hosted-ci.yml if absent. Returns True if created."""
145
+ workflows_dir = Path(workflows_dir)
146
+ target = workflows_dir / SELF_HOSTED_FILENAME
147
+ if target.exists():
148
+ return False
149
+ workflows_dir.mkdir(parents=True, exist_ok=True)
150
+ target.write_text(render_self_hosted_workflow(test_command, with_pull_request))
151
+ return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcli-framework
3
- Version: 8.0.47
3
+ Version: 8.0.49
4
4
  Summary: Portable workflow framework - transform any script into a versioned, schedulable command. Store in ~/.mcli/workflows/, version with lockfile, run as daemon or cron job.
5
5
  Author-email: Luis Fernandez de la Vara <luis@lefv.io>
6
6
  Maintainer-email: Luis Fernandez de la Vara <luis@lefv.io>
@@ -37,6 +37,7 @@ Requires-Python: >=3.10
37
37
  Description-Content-Type: text/markdown
38
38
  License-File: LICENSE
39
39
  Requires-Dist: click<9.0.0,>=8.1.7
40
+ Requires-Dist: ruamel.yaml<0.19,>=0.18
40
41
  Requires-Dist: rich<15.0.0,>=14.0.0
41
42
  Requires-Dist: requests<3.0.0,>=2.31.0
42
43
  Requires-Dist: tomli<3.0.0,>=2.2.1
@@ -165,6 +165,11 @@ src/mcli/workflow/__init__.py
165
165
  src/mcli/workflow/doc_convert.py
166
166
  src/mcli/workflow/lsh_integration.py
167
167
  src/mcli/workflow/workflow.py
168
+ src/mcli/workflow/ci/__init__.py
169
+ src/mcli/workflow/ci/act_runner.py
170
+ src/mcli/workflow/ci/ci.py
171
+ src/mcli/workflow/ci/runner_status.py
172
+ src/mcli/workflow/ci/workflow_transform.py
168
173
  src/mcli/workflow/daemon/__init__.py
169
174
  src/mcli/workflow/daemon/async_command_database.py
170
175
  src/mcli/workflow/daemon/async_process_manager.py
@@ -1,4 +1,5 @@
1
1
  click<9.0.0,>=8.1.7
2
+ ruamel.yaml<0.19,>=0.18
2
3
  rich<15.0.0,>=14.0.0
3
4
  requests<3.0.0,>=2.31.0
4
5
  tomli<3.0.0,>=2.2.1
File without changes