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.
- {runlayer-0.22.2 → runlayer-0.23.0}/PKG-INFO +3 -1
- {runlayer-0.22.2 → runlayer-0.23.0}/README.md +2 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/pyproject.toml +1 -1
- runlayer-0.23.0/runlayer_cli/commands/interactive_find.py +100 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/plugins.py +97 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/skills.py +93 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/deploy/docker_builder.py +33 -10
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/deploy/service.py +9 -2
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/env_substitution.py +1 -1
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_deploy_service.py +32 -1
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_docker_builder.py +105 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_env_substitution.py +14 -0
- runlayer-0.23.0/tests/test_interactive_find.py +20 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_plugins_commands.py +204 -19
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_skills_commands.py +142 -1
- {runlayer-0.22.2 → runlayer-0.23.0}/.gitignore +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/.python-version +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/AGENTS.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/CLAUDE.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/LICENSE +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/Makefile +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/development.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/hooks/README.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/hooks/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/hooks/runlayer-hook.sh +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/api.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/auth.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/cache.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/deploy.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/hooks.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/logs.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/org_api_key.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/scan.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/commands/setup.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/config.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/console.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/credential_store.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/deploy/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/deploy/config.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/index.html +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/logging.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/main.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/metrics.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/middleware.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/models.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/oauth.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/oauth_callback.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/paths.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/claude_manifest.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/discovery.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/installer.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/models.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/plugins/sync_engine.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/claude_code_plugins.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/clients.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/codex_plugins.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/config_parser.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/cursor_plugins.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/device.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/file_collector.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/openclaw_detector.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/opencode_plugins.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/plugin_scanner.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/project_scanner.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/service.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/scan/skill_scanner.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/AGENTS.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/CLAUDE.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/README.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/hashing.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/merkle.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/skill_id.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skill_identifier/types.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skills/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skills/discovery.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skills/installer.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skills/models.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/skills/sync_engine.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/symbols.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/sync.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/README.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/config.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/exceptions.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/proxy.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/verification/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/verification/base.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/verification/macos.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/runlayer_cli/verified_local_proxy/verification/windows.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/cache/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/cache/test_cache.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/conftest.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/deploy/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/deploy/test_lifecycle.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/deploy/test_validate.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/claude_code.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/claude_desktop.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/codex.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/cursor.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/goose.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/opencode.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/vscode.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/windsurf.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/client_specs/zed.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/.claude-plugin/marketplace.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/.claude-plugin/plugin.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/.lsp.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/.mcp.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/README.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/agents/README.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/agents/code-reviewer.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/commands/review.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/config.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/hooks/hooks.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/hooks/validate.sh +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/notes.txt +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/scripts/deploy.sh +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/settings.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skills/code-review/SKILL.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skills/code-review/prompts.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skills/ticket-triage/SKILL.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skills/ticket-triage/helper.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skills-v2/notes.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/skillsets/reference.md +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/fixtures/full_plugin/tool.ts +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/org_api_key/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/org_api_key/test_org_api_key.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/plugins/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/plugins/test_add.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/plugins/test_mcp_fallback.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/plugins/test_push.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/run/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/run/test_run.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/scan/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/scan/test_scan.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/setup/test_install_opencode.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/skills/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/skills/test_lifecycle.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/test_help_matrix.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/version/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/e2e/version/test_version.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/claude_code_config.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/claude_desktop_config.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/cursor_config.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/empty_config.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/goose_config.yaml +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/invalid_json.txt +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/sse_server_config.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/fixtures/vscode_config.json +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/skill_identifier/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/skill_identifier/test_merkle.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/skill_identifier/test_skill_id.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_api.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_auth.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_backwards_compatibility.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_cache.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_claude_code_plugins.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_claude_json_integration.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_cli.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_cli_backwards_compat.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_config.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_credential_store.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_cursor_plugins.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_hook_script.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_hooks_relay.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_logging.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_logs.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_metrics.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_middleware.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_oauth_browser_lock.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_oauth_token_storage.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_opencode_plugins.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_org_api_key_commands.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_plugin_scanner.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_plugins_discovery.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_plugins_installer.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_plugins_sync.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_clients.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_device.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_openclaw.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_parser.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_project.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_service.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_scan_skills.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_setup_hooks.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_setup_install_claude_desktop.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_setup_install_config_formats.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_setup_install_local_servers.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_skills_discovery.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_skills_installer.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_skills_sync.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/test_symbols.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_config.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_proxy.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_verification/__init__.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_verification/test_base.py +0 -0
- {runlayer-0.22.2 → runlayer-0.23.0}/tests/verified_local_proxy/test_verification/test_macos.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|