runlayer 0.22.2__tar.gz → 0.23.0__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 (205) hide show
  1. {runlayer-0.22.2 → runlayer-0.23.0}/PKG-INFO +3 -1
  2. {runlayer-0.22.2 → runlayer-0.23.0}/README.md +2 -0
  3. {runlayer-0.22.2 → runlayer-0.23.0}/pyproject.toml +1 -1
  4. runlayer-0.23.0/runlayer_cli/commands/interactive_find.py +100 -0
  5. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/plugins.py +97 -0
  6. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/skills.py +93 -0
  7. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/deploy/docker_builder.py +33 -10
  8. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/deploy/service.py +9 -2
  9. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/env_substitution.py +1 -1
  10. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_deploy_service.py +32 -1
  11. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_docker_builder.py +105 -0
  12. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_env_substitution.py +14 -0
  13. runlayer-0.23.0/tests/test_interactive_find.py +20 -0
  14. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_plugins_commands.py +204 -19
  15. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_skills_commands.py +142 -1
  16. {runlayer-0.22.2 → runlayer-0.23.0}/.gitignore +0 -0
  17. {runlayer-0.22.2 → runlayer-0.23.0}/.python-version +0 -0
  18. {runlayer-0.22.2 → runlayer-0.23.0}/AGENTS.md +0 -0
  19. {runlayer-0.22.2 → runlayer-0.23.0}/CLAUDE.md +0 -0
  20. {runlayer-0.22.2 → runlayer-0.23.0}/LICENSE +0 -0
  21. {runlayer-0.22.2 → runlayer-0.23.0}/Makefile +0 -0
  22. {runlayer-0.22.2 → runlayer-0.23.0}/development.md +0 -0
  23. {runlayer-0.22.2 → runlayer-0.23.0}/hooks/README.md +0 -0
  24. {runlayer-0.22.2 → runlayer-0.23.0}/hooks/__init__.py +0 -0
  25. {runlayer-0.22.2 → runlayer-0.23.0}/hooks/runlayer-hook.sh +0 -0
  26. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/__init__.py +0 -0
  27. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/api.py +0 -0
  28. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/__init__.py +0 -0
  29. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/auth.py +0 -0
  30. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/cache.py +0 -0
  31. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/deploy.py +0 -0
  32. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/hooks.py +0 -0
  33. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/logs.py +0 -0
  34. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/org_api_key.py +0 -0
  35. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/scan.py +0 -0
  36. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/setup.py +0 -0
  37. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/config.py +0 -0
  38. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/console.py +0 -0
  39. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/credential_store.py +0 -0
  40. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/deploy/__init__.py +0 -0
  41. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/deploy/config.py +0 -0
  42. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/index.html +0 -0
  43. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/logging.py +0 -0
  44. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/main.py +0 -0
  45. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/metrics.py +0 -0
  46. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/middleware.py +0 -0
  47. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/models.py +0 -0
  48. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/oauth.py +0 -0
  49. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/oauth_callback.py +0 -0
  50. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/paths.py +0 -0
  51. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/__init__.py +0 -0
  52. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/claude_manifest.py +0 -0
  53. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/discovery.py +0 -0
  54. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/installer.py +0 -0
  55. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/models.py +0 -0
  56. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/sync_engine.py +0 -0
  57. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/__init__.py +0 -0
  58. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/claude_code_plugins.py +0 -0
  59. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/clients.py +0 -0
  60. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/codex_plugins.py +0 -0
  61. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/config_parser.py +0 -0
  62. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/cursor_plugins.py +0 -0
  63. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/device.py +0 -0
  64. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/file_collector.py +0 -0
  65. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/openclaw_detector.py +0 -0
  66. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/opencode_plugins.py +0 -0
  67. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/plugin_scanner.py +0 -0
  68. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/project_scanner.py +0 -0
  69. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/service.py +0 -0
  70. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/skill_scanner.py +0 -0
  71. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/AGENTS.md +0 -0
  72. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/CLAUDE.md +0 -0
  73. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/README.md +0 -0
  74. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/__init__.py +0 -0
  75. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/hashing.py +0 -0
  76. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/merkle.py +0 -0
  77. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/skill_id.py +0 -0
  78. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/types.py +0 -0
  79. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skills/__init__.py +0 -0
  80. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skills/discovery.py +0 -0
  81. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skills/installer.py +0 -0
  82. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skills/models.py +0 -0
  83. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skills/sync_engine.py +0 -0
  84. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/symbols.py +0 -0
  85. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/sync.py +0 -0
  86. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/README.md +0 -0
  87. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/__init__.py +0 -0
  88. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/config.py +0 -0
  89. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/exceptions.py +0 -0
  90. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/proxy.py +0 -0
  91. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/verification/__init__.py +0 -0
  92. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/verification/base.py +0 -0
  93. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/verification/macos.py +0 -0
  94. {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/verification/windows.py +0 -0
  95. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/__init__.py +0 -0
  96. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/cache/__init__.py +0 -0
  97. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/cache/test_cache.py +0 -0
  98. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/conftest.py +0 -0
  99. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/deploy/__init__.py +0 -0
  100. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/deploy/test_lifecycle.py +0 -0
  101. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/deploy/test_validate.py +0 -0
  102. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/claude_code.md +0 -0
  103. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/claude_desktop.md +0 -0
  104. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/codex.md +0 -0
  105. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/cursor.md +0 -0
  106. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/goose.md +0 -0
  107. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/opencode.md +0 -0
  108. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/vscode.md +0 -0
  109. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/windsurf.md +0 -0
  110. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/zed.md +0 -0
  111. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/.claude-plugin/marketplace.json +0 -0
  112. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/.claude-plugin/plugin.json +0 -0
  113. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/.lsp.json +0 -0
  114. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/.mcp.json +0 -0
  115. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/README.md +0 -0
  116. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/agents/README.md +0 -0
  117. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/agents/code-reviewer.md +0 -0
  118. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/commands/review.md +0 -0
  119. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/config.json +0 -0
  120. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/hooks/hooks.json +0 -0
  121. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/hooks/validate.sh +0 -0
  122. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/notes.txt +0 -0
  123. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/scripts/deploy.sh +0 -0
  124. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/settings.json +0 -0
  125. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skills/code-review/SKILL.md +0 -0
  126. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skills/code-review/prompts.md +0 -0
  127. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skills/ticket-triage/SKILL.md +0 -0
  128. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skills/ticket-triage/helper.py +0 -0
  129. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skills-v2/notes.md +0 -0
  130. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skillsets/reference.md +0 -0
  131. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/tool.ts +0 -0
  132. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/org_api_key/__init__.py +0 -0
  133. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/org_api_key/test_org_api_key.py +0 -0
  134. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/plugins/__init__.py +0 -0
  135. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/plugins/test_add.py +0 -0
  136. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/plugins/test_mcp_fallback.py +0 -0
  137. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/plugins/test_push.py +0 -0
  138. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/run/__init__.py +0 -0
  139. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/run/test_run.py +0 -0
  140. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/scan/__init__.py +0 -0
  141. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/scan/test_scan.py +0 -0
  142. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/setup/test_install_opencode.py +0 -0
  143. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/skills/__init__.py +0 -0
  144. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/skills/test_lifecycle.py +0 -0
  145. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/test_help_matrix.py +0 -0
  146. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/version/__init__.py +0 -0
  147. {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/version/test_version.py +0 -0
  148. {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/claude_code_config.json +0 -0
  149. {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/claude_desktop_config.json +0 -0
  150. {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/cursor_config.json +0 -0
  151. {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/empty_config.json +0 -0
  152. {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/goose_config.yaml +0 -0
  153. {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/invalid_json.txt +0 -0
  154. {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/sse_server_config.json +0 -0
  155. {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/vscode_config.json +0 -0
  156. {runlayer-0.22.2 → runlayer-0.23.0}/tests/skill_identifier/__init__.py +0 -0
  157. {runlayer-0.22.2 → runlayer-0.23.0}/tests/skill_identifier/test_merkle.py +0 -0
  158. {runlayer-0.22.2 → runlayer-0.23.0}/tests/skill_identifier/test_skill_id.py +0 -0
  159. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_api.py +0 -0
  160. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_auth.py +0 -0
  161. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_backwards_compatibility.py +0 -0
  162. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_cache.py +0 -0
  163. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_claude_code_plugins.py +0 -0
  164. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_claude_json_integration.py +0 -0
  165. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_cli.py +0 -0
  166. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_cli_backwards_compat.py +0 -0
  167. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_config.py +0 -0
  168. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_credential_store.py +0 -0
  169. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_cursor_plugins.py +0 -0
  170. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_hook_script.py +0 -0
  171. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_hooks_relay.py +0 -0
  172. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_logging.py +0 -0
  173. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_logs.py +0 -0
  174. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_metrics.py +0 -0
  175. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_middleware.py +0 -0
  176. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_oauth_browser_lock.py +0 -0
  177. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_oauth_token_storage.py +0 -0
  178. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_opencode_plugins.py +0 -0
  179. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_org_api_key_commands.py +0 -0
  180. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_plugin_scanner.py +0 -0
  181. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_plugins_discovery.py +0 -0
  182. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_plugins_installer.py +0 -0
  183. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_plugins_sync.py +0 -0
  184. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_clients.py +0 -0
  185. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_device.py +0 -0
  186. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_openclaw.py +0 -0
  187. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_parser.py +0 -0
  188. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_project.py +0 -0
  189. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_service.py +0 -0
  190. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_skills.py +0 -0
  191. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_setup_hooks.py +0 -0
  192. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_setup_install_claude_desktop.py +0 -0
  193. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_setup_install_config_formats.py +0 -0
  194. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_setup_install_local_servers.py +0 -0
  195. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_skills_discovery.py +0 -0
  196. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_skills_installer.py +0 -0
  197. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_skills_sync.py +0 -0
  198. {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_symbols.py +0 -0
  199. {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/__init__.py +0 -0
  200. {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_config.py +0 -0
  201. {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_proxy.py +0 -0
  202. {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_verification/__init__.py +0 -0
  203. {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_verification/test_base.py +0 -0
  204. {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_verification/test_macos.py +0 -0
  205. {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_verification/test_windows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runlayer
3
- Version: 0.22.2
3
+ Version: 0.23.0
4
4
  Summary: A command-line interface for running MCP servers via HTTP transport
5
5
  Project-URL: Homepage, https://runlayer.com
6
6
  Project-URL: Documentation, https://docs.runlayer.com/
@@ -421,6 +421,8 @@ DATABASE_URL=postgres://localhost/db
421
421
  LOG_LEVEL=debug
422
422
  ```
423
423
 
424
+ **Limits:** Each `env` value in `runlayer.yaml` must be 2000 chars or less. If a value is too long, deploy validation fails and names the offending env var.
425
+
424
426
  **Note:** Variables from `.env` files override values from `os.environ`. The `$$VAR` syntax (double dollar sign) is reserved for backend variable substitution and will not be replaced by the CLI.
425
427
 
426
428
  ### `deploy validate` - Validate Configuration
@@ -186,6 +186,8 @@ DATABASE_URL=postgres://localhost/db
186
186
  LOG_LEVEL=debug
187
187
  ```
188
188
 
189
+ **Limits:** Each `env` value in `runlayer.yaml` must be 2000 chars or less. If a value is too long, deploy validation fails and names the offending env var.
190
+
189
191
  **Note:** Variables from `.env` files override values from `os.environ`. The `$$VAR` syntax (double dollar sign) is reserved for backend variable substitution and will not be replaced by the CLI.
190
192
 
191
193
  ### `deploy validate` - Validate Configuration
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "runlayer"
3
- version = "0.22.2"
3
+ version = "0.23.0"
4
4
  description = "A command-line interface for running MCP servers via HTTP transport"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -0,0 +1,100 @@
1
+ from collections.abc import Callable, Sequence
2
+ from typing import Literal, Protocol, TypeVar
3
+
4
+ import questionary
5
+ import typer
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ class _ChoiceDetail(Protocol):
11
+ name: str
12
+ namespace: str | None
13
+ description: str | None
14
+
15
+
16
+ def cancelled() -> None:
17
+ typer.echo("Cancelled.")
18
+ raise typer.Exit(0)
19
+
20
+
21
+ def format_choice(item: _ChoiceDetail) -> str:
22
+ line = item.name
23
+ if item.namespace:
24
+ line += f" ({item.namespace})"
25
+ if item.description:
26
+ line += f" - {item.description}"
27
+ return line
28
+
29
+
30
+ def prompt_items(
31
+ items: Sequence[T],
32
+ *,
33
+ noun: str,
34
+ format_item: Callable[[T], str],
35
+ ) -> list[T]:
36
+ if not items:
37
+ typer.echo(f"No {noun} available.")
38
+ raise typer.Exit(0)
39
+
40
+ selected = questionary.checkbox(
41
+ f"Select {noun} to install:",
42
+ choices=[
43
+ questionary.Choice(title=format_item(item), value=item) for item in items
44
+ ],
45
+ use_search_filter=True,
46
+ use_jk_keys=False,
47
+ instruction="(type to search, space to select)",
48
+ ).ask()
49
+ if not selected:
50
+ cancelled()
51
+ return list(selected)
52
+
53
+
54
+ def prompt_clients(client_names: Sequence[str]) -> list[str]:
55
+ selected = questionary.checkbox(
56
+ "Select clients:",
57
+ choices=[
58
+ questionary.Choice(
59
+ title=client_name.replace("_", " ").title(),
60
+ value=client_name,
61
+ checked=client_name == "claude_code",
62
+ )
63
+ for client_name in sorted(client_names)
64
+ ],
65
+ use_search_filter=True,
66
+ use_jk_keys=False,
67
+ instruction="(type to search, space to select)",
68
+ ).ask()
69
+ if not selected:
70
+ cancelled()
71
+ return [str(client_name) for client_name in selected]
72
+
73
+
74
+ def prompt_scope() -> Literal["project", "global"]:
75
+ selected = questionary.select(
76
+ "Install scope:",
77
+ choices=[
78
+ questionary.Choice(title="Project", value="project"),
79
+ questionary.Choice(title="Global", value="global"),
80
+ ],
81
+ use_jk_keys=False,
82
+ default="project",
83
+ ).ask()
84
+ if selected is None:
85
+ cancelled()
86
+ return selected
87
+
88
+
89
+ def confirm_install(
90
+ *,
91
+ item_count: int,
92
+ client_count: int,
93
+ item_label: str,
94
+ ) -> None:
95
+ confirmed = questionary.confirm(
96
+ f"Install {item_count} {item_label} to {client_count} client(s)?",
97
+ default=True,
98
+ ).ask()
99
+ if not confirmed:
100
+ cancelled()
@@ -3,8 +3,16 @@ from pathlib import Path
3
3
  import anyio
4
4
  import structlog
5
5
  import typer
6
+ from rich.console import Console
6
7
 
7
8
  from runlayer_cli.api import RunlayerClient
9
+ from runlayer_cli.commands.interactive_find import (
10
+ confirm_install,
11
+ format_choice,
12
+ prompt_clients,
13
+ prompt_items,
14
+ prompt_scope,
15
+ )
8
16
  from runlayer_cli.console import print_error
9
17
  from runlayer_cli.config import resolve_credentials, set_credentials_in_context
10
18
  from runlayer_cli.logging import setup_logging
@@ -26,6 +34,7 @@ from runlayer_cli.plugins.sync_engine import (
26
34
  )
27
35
 
28
36
  logger = structlog.get_logger(__name__)
37
+ console = Console()
29
38
 
30
39
  app = typer.Typer(help="Manage plugins")
31
40
 
@@ -197,6 +206,94 @@ def push(
197
206
  raise typer.Exit(1)
198
207
 
199
208
 
209
+ @app.command()
210
+ def find(
211
+ ctx: typer.Context,
212
+ secret: str | None = typer.Option(
213
+ None, "--secret", "-s", envvar="RUNLAYER_API_KEY"
214
+ ),
215
+ host: str | None = typer.Option(None, "--host", "-H", envvar="RUNLAYER_HOST"),
216
+ ) -> None:
217
+ """Find and install one plugin from Runlayer API."""
218
+ log_file_path = setup_logging(command="plugins-find", quiet_console=False)
219
+
220
+ set_credentials_in_context(ctx, secret, host)
221
+ credentials = resolve_credentials(ctx, require_auth=True)
222
+
223
+ try:
224
+ api = RunlayerClient(hostname=credentials["host"], secret=credentials["secret"])
225
+ with console.status("Loading plugins..."):
226
+ plugins = sorted(
227
+ api.list_all_plugins(mine_only=False),
228
+ key=lambda plugin: (
229
+ plugin.name.lower(),
230
+ (plugin.namespace or "").lower(),
231
+ ),
232
+ )
233
+ selected_plugins = prompt_items(
234
+ plugins,
235
+ noun="plugins",
236
+ format_item=lambda plugin: format_choice(plugin),
237
+ )
238
+ resolved_clients = prompt_clients(list(_SUPPORTED_CLIENTS))
239
+ install_scope = prompt_scope()
240
+ global_install = install_scope == "global"
241
+ confirm_install(
242
+ item_count=len(selected_plugins),
243
+ client_count=len(resolved_clients),
244
+ item_label="plugin(s)",
245
+ )
246
+
247
+ async def _run() -> PluginInstallResult:
248
+ combined = PluginInstallResult()
249
+ for resolved_client in resolved_clients:
250
+ canonical, editor, lockfile = resolve_plugin_dirs(
251
+ resolved_client, global_install, Path.cwd()
252
+ )
253
+
254
+ def on_progress(name: str, status: str) -> None:
255
+ typer.echo(f" {resolved_client} / {name}: {status}")
256
+
257
+ for selected_plugin in selected_plugins:
258
+ result = await install_plugins(
259
+ client=api,
260
+ source=selected_plugin.id,
261
+ install_all=False,
262
+ plugin_name=None,
263
+ canonical_dir=canonical,
264
+ editor_dir=editor,
265
+ lockfile_path=lockfile,
266
+ client_name=resolved_client,
267
+ host=credentials["host"],
268
+ install_scope=install_scope,
269
+ dry_run=False,
270
+ on_progress=on_progress,
271
+ )
272
+ combined.installed.extend(result.installed)
273
+ combined.skipped.extend(result.skipped)
274
+ combined.errors.extend(result.errors)
275
+ return combined
276
+
277
+ result = anyio.run(_run)
278
+ parts = []
279
+ if result.installed:
280
+ parts.append(f"{len(result.installed)} installed")
281
+ if result.skipped:
282
+ parts.append(f"{len(result.skipped)} skipped")
283
+ typer.echo(f"Done: {', '.join(parts) if parts else 'nothing to do'}")
284
+
285
+ if result.errors:
286
+ for err in result.errors:
287
+ typer.secho(f" Error: {err}", fg=typer.colors.RED, err=True)
288
+ raise typer.Exit(1)
289
+ except typer.Exit:
290
+ raise
291
+ except Exception as e:
292
+ logger.error("find_failed", error=str(e), exc_info=True)
293
+ print_error(str(e), str(log_file_path))
294
+ raise typer.Exit(1)
295
+
296
+
200
297
  @app.command(
201
298
  name="list",
202
299
  help=(
@@ -5,11 +5,19 @@ from pathlib import Path
5
5
  import anyio
6
6
  import structlog
7
7
  import typer
8
+ from rich.console import Console
8
9
 
9
10
  from runlayer_cli.api import RunlayerClient, SkillScanResponse
10
11
  from runlayer_cli.console import print_error
11
12
  from runlayer_cli.config import resolve_credentials, set_credentials_in_context
12
13
  from runlayer_cli.logging import setup_logging
14
+ from runlayer_cli.commands.interactive_find import (
15
+ confirm_install,
16
+ format_choice,
17
+ prompt_clients,
18
+ prompt_items,
19
+ prompt_scope,
20
+ )
13
21
  from runlayer_cli.skills.discovery import discover_skills
14
22
  from runlayer_cli.skills.models import DiscoveredSkill
15
23
  from runlayer_cli.skills.installer import (
@@ -25,6 +33,7 @@ from runlayer_cli.skills.installer import (
25
33
  from runlayer_cli.skills.sync_engine import SyncResult, sync_skills
26
34
 
27
35
  logger = structlog.get_logger(__name__)
36
+ console = Console()
28
37
 
29
38
  app = typer.Typer(help="Manage skills")
30
39
 
@@ -245,6 +254,90 @@ def scan(
245
254
  raise typer.Exit(1)
246
255
 
247
256
 
257
+ @app.command()
258
+ def find(
259
+ ctx: typer.Context,
260
+ secret: str | None = typer.Option(
261
+ None, "--secret", "-s", envvar="RUNLAYER_API_KEY"
262
+ ),
263
+ host: str | None = typer.Option(None, "--host", "-H", envvar="RUNLAYER_HOST"),
264
+ ) -> None:
265
+ """Find and install one skill from Runlayer API."""
266
+ log_file_path = setup_logging(command="skills-find", quiet_console=False)
267
+
268
+ set_credentials_in_context(ctx, secret, host)
269
+ credentials = resolve_credentials(ctx, require_auth=True)
270
+
271
+ try:
272
+ api = RunlayerClient(hostname=credentials["host"], secret=credentials["secret"])
273
+ with console.status("Loading skills..."):
274
+ skills = sorted(
275
+ api.list_all_skills(mine_only=False),
276
+ key=lambda skill: (skill.name.lower(), (skill.namespace or "").lower()),
277
+ )
278
+ selected_skills = prompt_items(
279
+ skills,
280
+ noun="skills",
281
+ format_item=lambda skill: format_choice(skill),
282
+ )
283
+ resolved_clients = prompt_clients(list(SKILLS_DIR_MAP))
284
+ install_scope = prompt_scope()
285
+ global_install = install_scope == "global"
286
+ confirm_install(
287
+ item_count=len(selected_skills),
288
+ client_count=len(resolved_clients),
289
+ item_label="skill(s)",
290
+ )
291
+
292
+ async def _run() -> InstallResult:
293
+ combined = InstallResult()
294
+ for resolved_client in resolved_clients:
295
+ canonical, editor, lockfile = resolve_dirs(
296
+ resolved_client, global_install, Path.cwd()
297
+ )
298
+
299
+ def on_progress(name: str, status: str) -> None:
300
+ typer.echo(f" {resolved_client} / {name}: {status}")
301
+
302
+ for selected_skill in selected_skills:
303
+ result = await install_skills(
304
+ client=api,
305
+ source=selected_skill.id,
306
+ install_all=False,
307
+ skill_name=None,
308
+ canonical_dir=canonical,
309
+ editor_dir=editor,
310
+ lockfile_path=lockfile,
311
+ client_name=resolved_client,
312
+ install_scope=install_scope,
313
+ dry_run=False,
314
+ on_progress=on_progress,
315
+ )
316
+ combined.installed.extend(result.installed)
317
+ combined.skipped.extend(result.skipped)
318
+ combined.errors.extend(result.errors)
319
+ return combined
320
+
321
+ result = anyio.run(_run)
322
+ parts = []
323
+ if result.installed:
324
+ parts.append(f"{len(result.installed)} installed")
325
+ if result.skipped:
326
+ parts.append(f"{len(result.skipped)} skipped")
327
+ typer.echo(f"Done: {', '.join(parts) if parts else 'nothing to do'}")
328
+
329
+ if result.errors:
330
+ for err in result.errors:
331
+ typer.secho(f" Error: {err}", fg=typer.colors.RED, err=True)
332
+ raise typer.Exit(1)
333
+ except typer.Exit:
334
+ raise
335
+ except Exception as e:
336
+ logger.error("find_failed", error=str(e), exc_info=True)
337
+ print_error(str(e), str(log_file_path))
338
+ raise typer.Exit(1)
339
+
340
+
248
341
  @app.command(name="list")
249
342
  def list_skills(
250
343
  client_name: str | None = typer.Option(
@@ -45,13 +45,31 @@ def check_docker_available() -> bool:
45
45
  return False
46
46
 
47
47
 
48
- def authenticate_ecr(credentials: ECRCredentials) -> None:
48
+ def get_registry_hostname(registry_url: str) -> str:
49
+ """Normalize registry URLs to the hostname Docker expects."""
50
+ return registry_url.replace("https://", "").replace("http://", "")
51
+
52
+
53
+ def get_registry_auth_config(credentials: ECRCredentials) -> dict[str, str]:
54
+ """Build request-scoped registry auth for Docker SDK calls."""
55
+ registry = get_registry_hostname(credentials.registry_url)
56
+ return {
57
+ "username": credentials.username,
58
+ "password": credentials.password,
59
+ "serveraddress": registry,
60
+ }
61
+
62
+
63
+ def authenticate_ecr(credentials: ECRCredentials) -> dict[str, str]:
49
64
  """
50
65
  Authenticate Docker with ECR using provided credentials.
51
66
 
52
67
  Args:
53
68
  credentials: ECR credentials from backend
54
69
 
70
+ Returns:
71
+ Auth config that can be reused by later Docker SDK calls
72
+
55
73
  Raises:
56
74
  DockerException: If authentication fails
57
75
  """
@@ -73,11 +91,8 @@ def authenticate_ecr(credentials: ECRCredentials) -> None:
73
91
  "Please get fresh credentials and try again."
74
92
  )
75
93
 
76
- client = docker.from_env()
77
-
78
- registry = credentials.registry_url.replace("https://", "").replace(
79
- "http://", ""
80
- )
94
+ registry = get_registry_hostname(credentials.registry_url)
95
+ auth_config = get_registry_auth_config(credentials)
81
96
 
82
97
  with Progress(
83
98
  SpinnerColumn(),
@@ -121,6 +136,7 @@ def authenticate_ecr(credentials: ECRCredentials) -> None:
121
136
  console.print("[dim]Retrying with Docker SDK instead...[/dim]")
122
137
 
123
138
  try:
139
+ client = docker.from_env()
124
140
  client.login(
125
141
  username=credentials.username,
126
142
  password=credentials.password,
@@ -133,6 +149,8 @@ def authenticate_ecr(credentials: ECRCredentials) -> None:
133
149
  except Exception as sdk_error:
134
150
  raise DockerException(f"Failed to authenticate: {sdk_error}")
135
151
 
152
+ return auth_config
153
+
136
154
  except APIError as e:
137
155
  raise DockerException(f"Failed to authenticate with ECR: {e}")
138
156
 
@@ -274,12 +292,13 @@ def tag_image(image_id: str, repository: str, tag: str) -> str:
274
292
  raise DockerException(f"Failed to tag image: {e}")
275
293
 
276
294
 
277
- def push_image(image_tag: str) -> str:
295
+ def push_image(image_tag: str, auth_config: Optional[dict[str, str]] = None) -> str:
278
296
  """
279
297
  Push a Docker image to a registry and get its digest.
280
298
 
281
299
  Args:
282
300
  image_tag: Full image tag (e.g., "registry/repo:tag")
301
+ auth_config: Optional request-scoped auth for the registry
283
302
 
284
303
  Returns:
285
304
  Image digest (SHA256 hash) of the pushed image
@@ -302,7 +321,11 @@ def push_image(image_tag: str) -> str:
302
321
  ) as progress:
303
322
  task = progress.add_task(description="Starting push...", total=None)
304
323
 
305
- for line in client.images.push(image_tag, stream=True, decode=True):
324
+ push_kwargs: dict[str, Any] = {"stream": True, "decode": True}
325
+ if auth_config:
326
+ push_kwargs["auth_config"] = auth_config
327
+
328
+ for line in client.images.push(image_tag, **push_kwargs):
306
329
  if "error" in line:
307
330
  error_msg = line.get("error", "Unknown error")
308
331
  console.print(f"\n[red]ERROR: {error_msg}[/red]\n")
@@ -387,7 +410,7 @@ def build_and_push(
387
410
  DockerPushError: If push fails
388
411
  """
389
412
  # Authenticate with ECR
390
- authenticate_ecr(credentials)
413
+ auth_config = authenticate_ecr(credentials)
391
414
 
392
415
  # Build the image with a local tag first
393
416
  local_tag = f"runlayer-build:{tag}"
@@ -404,6 +427,6 @@ def build_and_push(
404
427
  full_image_uri = tag_image(image_id, repository, tag)
405
428
 
406
429
  # Push to ECR and get digest
407
- image_digest = push_image(full_image_uri)
430
+ image_digest = push_image(full_image_uri, auth_config=auth_config)
408
431
 
409
432
  return full_image_uri, image_digest
@@ -293,11 +293,18 @@ def deploy_service(
293
293
  else:
294
294
  time_str = f"{seconds}s"
295
295
 
296
+ deployment_url = f"{host}/deploy/{deployment_id}"
297
+
296
298
  typer.secho(
297
299
  f"\n⏱️ Total deployment time: {time_str}",
298
300
  fg=typer.colors.CYAN,
299
301
  bold=True,
300
302
  )
303
+ typer.secho(
304
+ f"🔗 View deployment: {deployment_url}",
305
+ fg=typer.colors.CYAN,
306
+ bold=True,
307
+ )
301
308
 
302
309
 
303
310
  def _get_or_create_deployment(
@@ -485,9 +492,9 @@ def _push_to_ecr(
485
492
 
486
493
  typer.echo("Pushing image to ECR...\n")
487
494
  try:
488
- authenticate_ecr(ecr_creds)
495
+ auth_config = authenticate_ecr(ecr_creds)
489
496
  full_image_uri = tag_image(image_id, repository, deployment_id)
490
- image_digest = push_image(full_image_uri)
497
+ image_digest = push_image(full_image_uri, auth_config=auth_config)
491
498
 
492
499
  # Use digest-based reference for immutable deployments
493
500
  # This ensures ECS detects changes even with the same tag
@@ -57,7 +57,7 @@ def load_env_vars(
57
57
  # Load .env file if found (overrides os.environ)
58
58
  if env_path:
59
59
  # Load .env file into a dict (doesn't modify os.environ)
60
- dotenv_vars = dotenv_values(env_path)
60
+ dotenv_vars = dotenv_values(env_path, encoding="utf-8-sig")
61
61
 
62
62
  # Merge dotenv_vars into env_vars (dotenv file overrides os.environ)
63
63
  # Filter out None values (unset variables in .env file)
@@ -225,6 +225,11 @@ def test_push_to_ecr_success(mock_api_client):
225
225
  patch("runlayer_cli.deploy.service.push_image") as mock_push,
226
226
  patch("runlayer_cli.deploy.service.typer"),
227
227
  ):
228
+ mock_auth.return_value = {
229
+ "username": "AWS",
230
+ "password": "test-password",
231
+ "serveraddress": "123456789.dkr.ecr.us-east-1.amazonaws.com",
232
+ }
228
233
  mock_tag.return_value = (
229
234
  "123456789.dkr.ecr.us-east-1.amazonaws.com/my-repo:test-deployment-id"
230
235
  )
@@ -238,7 +243,10 @@ def test_push_to_ecr_success(mock_api_client):
238
243
  assert result == expected_uri
239
244
  mock_auth.assert_called_once()
240
245
  mock_tag.assert_called_once()
241
- mock_push.assert_called_once()
246
+ mock_push.assert_called_once_with(
247
+ "123456789.dkr.ecr.us-east-1.amazonaws.com/my-repo:test-deployment-id",
248
+ auth_config=mock_auth.return_value,
249
+ )
242
250
 
243
251
 
244
252
  def test_update_deployment_config_success(mock_api_client):
@@ -504,6 +512,29 @@ def test_validate_runlayer_yaml_config_http_error(mock_api_client):
504
512
  assert exc_info.value.exit_code == 1
505
513
 
506
514
 
515
+ def test_validate_runlayer_yaml_config_shows_backend_env_limit_error(mock_api_client):
516
+ """Test backend env limit error is surfaced unchanged."""
517
+ yaml_content = "name: test-service\nruntime: docker\nservice:\n port: 8000\n"
518
+
519
+ mock_response = ValidateYAMLResponse(
520
+ valid=False,
521
+ error="Configuration error: env.API_KEY is too long (2001 chars, max 2000)",
522
+ parsed_config=None,
523
+ )
524
+ mock_api_client.validate_yaml.return_value = mock_response
525
+
526
+ with (
527
+ patch("runlayer_cli.deploy.service.typer.echo") as _mock_echo,
528
+ patch("runlayer_cli.deploy.service.typer.secho") as mock_secho,
529
+ ):
530
+ with pytest.raises(typer.Exit) as exc_info:
531
+ _validate_runlayer_yaml_config(mock_api_client, yaml_content)
532
+
533
+ assert exc_info.value.exit_code == 1
534
+ rendered_calls = [str(call) for call in mock_secho.call_args_list]
535
+ assert any("env.API_KEY is too long" in call for call in rendered_calls)
536
+
537
+
507
538
  def test_validate_service_success():
508
539
  """Test successful validation service call."""
509
540
  with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: