ellf-cli 4.0.150__tar.gz → 5.0.4__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.
- {ellf_cli-4.0.150/ellf_cli.egg-info → ellf_cli-5.0.4}/PKG-INFO +1 -1
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/README.md +11 -10
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/about.json +1 -1
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/auth.py +17 -4
- ellf_cli-5.0.4/ellf_cli/commands/_cluster_select.py +96 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/clusters.py +132 -8
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/config.py +0 -20
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/general.py +27 -2
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf.json +50 -37
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/messages.py +3 -3
- {ellf_cli-4.0.150 → ellf_cli-5.0.4/ellf_cli.egg-info}/PKG-INFO +1 -1
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli.egg-info/SOURCES.txt +1 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/LICENSE +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/MANIFEST.in +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/__init__.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/__main__.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/about.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/appdirs.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/cli.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/cloud/__init__.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/cloud/gcp.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/cluster_config.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/__init__.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/_recipe_file.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/_recipe_subcommand.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/_state.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/actions.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/agents.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/assets.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/auth.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/datasets.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/files/__init__.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/files/cp.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/files/ls.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/files/rm.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/files/rsync.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/files/stats.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/import_export.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/infra/__init__.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/infra/_helpers.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/infra/deploy.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/infra/init_values.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/infra/provision.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/infra/register.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/infra/setup.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/infra/start.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/infra/terraform.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/infra/tls.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/jobs.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/packages.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/paths.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/plans.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/projects.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/publish_code.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/publish_data.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/recipes.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/secrets.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/support.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/tasks.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/commands/todos.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/config.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/.claude-plugin/plugin.json +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/.gitignore +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/bin/write-current-session.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/hooks/hooks.json +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skill_variants.json +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/annotation_audit.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/builtin_ellf_annotation_recipes.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/annotation_audit.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/builtin_ellf_annotation_recipes.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/builtin_prodigy_recipes.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-ask/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-handoff/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/annotation_metrics.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/training_monitoring.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/annotation_metrics.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/training_monitoring.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/scripts/check_training.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-ops.assistant/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-ops.coding/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-ops.coding/scripts/run_job.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-patterns/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-patterns/references/pattern_strategies.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_action_recipe.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_agent_recipe.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_blocks_ui.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_correct.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_custom_ui.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_manual.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_pages_ui.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_routing.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_task_recipe.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_teach.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/builtin_recipes.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/ellf_recipe_sdk.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/lint_recipe.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/prodigy_recipe_api.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/template_index.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/consulting_patterns.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/explosion_strategy.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/prodigy_llm_bot.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/consulting_patterns.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/explosion_strategy.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/prodigy_llm_bot.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-support.assistant/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-support.coding/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-todo/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/diagnostics.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/evaluation_guide.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/model_selection.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/training_paradigms.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/workflow.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/SKILL.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_advanced.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_architectures.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_training.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/diagnostics.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/evaluation_guide.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/experiment_patterns.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/model_selection.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_paradigms.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_troubleshooting.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/workflow.md +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/scripts/ellf_logger.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/errors.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/helm.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/key_pair.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/main.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/query.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/recipes_cookiecutter/cookiecutter.json +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/.gitignore +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/README.md.tmpl +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements-dev.in +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements.in +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/setup.py.tmpl +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/__init__.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/about.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/recipes/__init__.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/recipes/example_task.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/testing/__init__.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ty.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/ui.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/url.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli/util.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli.egg-info/dependency_links.txt +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli.egg-info/entry_points.txt +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli.egg-info/not-zip-safe +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli.egg-info/requires.txt +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/ellf_cli.egg-info/top_level.txt +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/pyproject.toml +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/setup.cfg +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/setup.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_appdirs.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_auth.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_config.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_errors.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_files_cp.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_files_cp_helpers.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_info.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_invalid_secrets.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_key_pair.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_login.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_main.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_plans.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_projects.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_query.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_recipe_file.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_recipes.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_state.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_support.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_ty.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_ui.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_ui_extras.py +0 -0
- {ellf_cli-4.0.150 → ellf_cli-5.0.4}/tests/test_util.py +0 -0
|
@@ -326,6 +326,15 @@ List resources on the cluster
|
|
|
326
326
|
| `--select` | `list[str]` | Comma-separated fields to select and show in output. Available: ['id', 'created', 'updated', 'org_id', 'name', 'address', 'state'] | `['id', 'name', 'status', 'address']` |
|
|
327
327
|
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
328
328
|
|
|
329
|
+
#### `ellf clusters use`
|
|
330
|
+
|
|
331
|
+
Switch the active cluster. Looks up the cluster by name or ID via PAM. With no argument and multiple clusters available, prompts interactively in a TTY.
|
|
332
|
+
|
|
333
|
+
| Argument | Type | Description | Default |
|
|
334
|
+
| --- | --- | --- | --- |
|
|
335
|
+
| `name_or_id` | `str` | Name or ID of the cluster | `None` |
|
|
336
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
337
|
+
|
|
329
338
|
#### `ellf clusters info`
|
|
330
339
|
|
|
331
340
|
Get detailed info for a cluster
|
|
@@ -357,7 +366,7 @@ Delete a cluster from PAM. This only removes PAM's record of it. The cluster its
|
|
|
357
366
|
|
|
358
367
|
#### `ellf clusters check`
|
|
359
368
|
|
|
360
|
-
Check the health of a cluster deployment. Runs CLI-side connectivity checks against the cluster and PAM. Use --deep to also trigger cluster-side deployment checks (K8s API, NFS,
|
|
369
|
+
Check the health of a cluster deployment. Runs CLI-side connectivity checks against the cluster and PAM. Use --deep (or supply --s3-bucket / --nfs-path / --recipe) to also trigger cluster-side deployment checks via the broker's /v1/check endpoints (K8s API, NFS, S3, recipe execution, DB).
|
|
361
370
|
|
|
362
371
|
| Argument | Type | Description | Default |
|
|
363
372
|
| --- | --- | --- | --- |
|
|
@@ -473,15 +482,6 @@ Set the default agent.
|
|
|
473
482
|
| `cluster_id` | `UUID` | ID of the cluster to search for agent name (or the last cluster if not set) | `None` |
|
|
474
483
|
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
475
484
|
|
|
476
|
-
#### `ellf config set-cluster-host`
|
|
477
|
-
|
|
478
|
-
Set the cluster cluster host.
|
|
479
|
-
|
|
480
|
-
| Argument | Type | Description | Default |
|
|
481
|
-
| --- | --- | --- | --- |
|
|
482
|
-
| `host` | `str` | Host or URL of the cluster | |
|
|
483
|
-
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
484
|
-
|
|
485
485
|
#### `ellf config set-pam-host`
|
|
486
486
|
|
|
487
487
|
Set the PAM host.
|
|
@@ -1070,6 +1070,7 @@ Log in to your Ellf account. You normally don't need to call this manually. It w
|
|
|
1070
1070
|
| --- | --- | --- | --- |
|
|
1071
1071
|
| `--no-cluster` | `bool` | Don't use a cluster | `False` |
|
|
1072
1072
|
| `--no-browser` | `bool` | Don't open a browser, just print the login URL | `False` |
|
|
1073
|
+
| `--cluster` | `str` | Name or ID of the cluster to log into. If omitted: auto-select when only one is available, or prompt interactively | `None` |
|
|
1073
1074
|
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1074
1075
|
| `--claude` | `bool` | Install Ellf Claude Code skills and transcript hook into ~/.claude/ | `False` |
|
|
1075
1076
|
|
|
@@ -164,6 +164,8 @@ class AuthState(Protocol):
|
|
|
164
164
|
|
|
165
165
|
def _ensure_broker_host(self) -> None: ...
|
|
166
166
|
|
|
167
|
+
def set_active_cluster(self, cluster_id: UUID, broker_url: URL) -> None: ...
|
|
168
|
+
|
|
167
169
|
def get_id_token(self, force_refresh: bool = False) -> str: ...
|
|
168
170
|
|
|
169
171
|
def get_api_token(self, force_refresh: bool = False) -> AccessTokenCredential: ...
|
|
@@ -341,13 +343,24 @@ class AuthStateImpl:
|
|
|
341
343
|
raise CLIError(Messages.E106)
|
|
342
344
|
elif len(clusters) == 1:
|
|
343
345
|
self._broker_url = URL.parse(clusters[0].address)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
)
|
|
346
|
+
self._cluster_id = clusters[0].id
|
|
347
|
+
settings = get_saved_settings()
|
|
348
|
+
settings.update("broker_host", self.broker_host)
|
|
349
|
+
settings.update("cluster_id", self._cluster_id)
|
|
348
350
|
else:
|
|
349
351
|
raise CLIError(Messages.E107)
|
|
350
352
|
|
|
353
|
+
def set_active_cluster(self, cluster_id: UUID, broker_url: URL) -> None:
|
|
354
|
+
"""Switch the active cluster in-memory.
|
|
355
|
+
|
|
356
|
+
Persisting to ``SavedSettings`` is the caller's responsibility — this
|
|
357
|
+
only updates the auth state so the next ``broker_client`` /
|
|
358
|
+
``get_cluster_token`` call targets the new cluster.
|
|
359
|
+
"""
|
|
360
|
+
self._broker_url = broker_url
|
|
361
|
+
self._cluster_id = cluster_id
|
|
362
|
+
self._cluster_client = None
|
|
363
|
+
|
|
351
364
|
@staticmethod
|
|
352
365
|
def _check_token_expired(
|
|
353
366
|
token: AccessTokenCredential | BrokerAccessTokenCredential | str,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Union
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from wasabi import msg
|
|
9
|
+
|
|
10
|
+
from ellf_pam_sdk import Client
|
|
11
|
+
from ellf_pam_sdk.models import ClusterSummary
|
|
12
|
+
|
|
13
|
+
from ..errors import CLIError
|
|
14
|
+
from ..messages import Messages
|
|
15
|
+
from ..ui import isatty
|
|
16
|
+
from ..url import URL, URLError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_address(address: str) -> str | None:
|
|
20
|
+
"""Normalize an address-shaped string. Returns ``None`` when unparseable.
|
|
21
|
+
|
|
22
|
+
Pam-side and user-supplied URLs vary on trailing slashes and implicit
|
|
23
|
+
schemes, so we compare them in canonical form before deciding a
|
|
24
|
+
`--cluster <url>` argument matches a registered cluster.
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
return str(URL.parse(address))
|
|
28
|
+
except URLError:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _match_cluster(
|
|
33
|
+
clusters: builtins.list[ClusterSummary],
|
|
34
|
+
name_or_id: Union[str, UUID],
|
|
35
|
+
) -> ClusterSummary | None:
|
|
36
|
+
if isinstance(name_or_id, UUID):
|
|
37
|
+
for c in clusters:
|
|
38
|
+
if c.id == name_or_id:
|
|
39
|
+
return c
|
|
40
|
+
return None
|
|
41
|
+
needle = str(name_or_id)
|
|
42
|
+
for c in clusters:
|
|
43
|
+
if str(c.id) == needle or c.name == needle or c.address == needle:
|
|
44
|
+
return c
|
|
45
|
+
needle_url = _normalize_address(needle)
|
|
46
|
+
if needle_url is None:
|
|
47
|
+
return None
|
|
48
|
+
for c in clusters:
|
|
49
|
+
canonical = _normalize_address(c.address)
|
|
50
|
+
if canonical is not None and canonical == needle_url:
|
|
51
|
+
return c
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _prompt_for_cluster(
|
|
56
|
+
clusters: builtins.list[ClusterSummary],
|
|
57
|
+
) -> ClusterSummary:
|
|
58
|
+
if not isatty(sys.stdin) or not isatty(sys.stdout):
|
|
59
|
+
raise CLIError(Messages.E107)
|
|
60
|
+
msg.info("Multiple clusters are available:")
|
|
61
|
+
for idx, cluster in enumerate(clusters, start=1):
|
|
62
|
+
print(f" [{idx}] {cluster.name} ({cluster.address})")
|
|
63
|
+
while True:
|
|
64
|
+
raw = input(f"Pick a cluster [1-{len(clusters)}]: ").strip()
|
|
65
|
+
if raw.isdigit():
|
|
66
|
+
choice = int(raw)
|
|
67
|
+
if 1 <= choice <= len(clusters):
|
|
68
|
+
return clusters[choice - 1]
|
|
69
|
+
matched = _match_cluster(clusters, raw)
|
|
70
|
+
if matched is not None:
|
|
71
|
+
return matched
|
|
72
|
+
msg.warn(f"Invalid selection: {raw!r}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def select_cluster(
|
|
76
|
+
client: Client,
|
|
77
|
+
name_or_id: Union[str, UUID, None] = None,
|
|
78
|
+
) -> ClusterSummary:
|
|
79
|
+
"""Pick a cluster the user has access to.
|
|
80
|
+
|
|
81
|
+
When ``name_or_id`` is provided, looks it up exactly. Otherwise:
|
|
82
|
+
auto-selects the only cluster, or prompts in a TTY when there are
|
|
83
|
+
multiple. Raises ``CLIError`` if no cluster is accessible or the
|
|
84
|
+
caller is non-interactive with multiple clusters.
|
|
85
|
+
"""
|
|
86
|
+
clusters = builtins.list(client.cluster.all(page_size=100))
|
|
87
|
+
if not clusters:
|
|
88
|
+
raise CLIError(Messages.E106)
|
|
89
|
+
if name_or_id is not None:
|
|
90
|
+
matched = _match_cluster(clusters, name_or_id)
|
|
91
|
+
if matched is None:
|
|
92
|
+
raise CLIError(Messages.E038.format(noun="cluster", name_or_id=name_or_id))
|
|
93
|
+
return matched
|
|
94
|
+
if len(clusters) == 1:
|
|
95
|
+
return clusters[0]
|
|
96
|
+
return _prompt_for_cluster(clusters)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import builtins
|
|
3
|
+
import json
|
|
3
4
|
import subprocess
|
|
5
|
+
import time
|
|
4
6
|
import uuid
|
|
5
7
|
from typing import Any, Optional, Union
|
|
6
8
|
from uuid import UUID
|
|
@@ -10,6 +12,7 @@ import uuid_utils
|
|
|
10
12
|
from radicli import Arg
|
|
11
13
|
from wasabi import msg
|
|
12
14
|
|
|
15
|
+
from ellf_broker_sdk.models import CheckProgressRequest, CheckStartRequest
|
|
13
16
|
from ellf_pam_sdk import models as ellf_pam_sdk_models
|
|
14
17
|
from ellf_pam_sdk.models import (
|
|
15
18
|
ClusterUpdating,
|
|
@@ -24,11 +27,12 @@ from ..errors import (
|
|
|
24
27
|
RequestError,
|
|
25
28
|
)
|
|
26
29
|
from ..messages import Messages
|
|
27
|
-
from ..query import resolve_cluster, resolve_cluster_id
|
|
30
|
+
from ..query import resolve_cluster, resolve_cluster_id, resolve_recipe
|
|
28
31
|
from ..ty import ClusterStatusCheck
|
|
29
|
-
from ..ui import print_info_table, print_table_with_select
|
|
32
|
+
from ..ui import print_info_table, print_mutation_result, print_table_with_select
|
|
30
33
|
from ..util import URL
|
|
31
|
-
from .
|
|
34
|
+
from ._cluster_select import select_cluster
|
|
35
|
+
from ._state import get_auth_state, get_root_cfg, get_saved_settings
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
def uuid7() -> uuid.UUID:
|
|
@@ -129,6 +133,42 @@ def list(
|
|
|
129
133
|
return res
|
|
130
134
|
|
|
131
135
|
|
|
136
|
+
@cli.subcommand(
|
|
137
|
+
"clusters",
|
|
138
|
+
"use",
|
|
139
|
+
name_or_id=Arg(help=Messages.name_or_id.format(noun="cluster")),
|
|
140
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
141
|
+
)
|
|
142
|
+
def use(
|
|
143
|
+
name_or_id: Optional[Union[str, UUID]] = None,
|
|
144
|
+
as_json: bool = False,
|
|
145
|
+
) -> UUID:
|
|
146
|
+
"""Switch the active cluster.
|
|
147
|
+
|
|
148
|
+
Looks up the cluster by name or ID via PAM. With no argument and
|
|
149
|
+
multiple clusters available, prompts interactively in a TTY.
|
|
150
|
+
"""
|
|
151
|
+
auth = get_auth_state()
|
|
152
|
+
chosen = select_cluster(auth.client, name_or_id)
|
|
153
|
+
broker_url = URL.parse(chosen.address)
|
|
154
|
+
settings = get_saved_settings()
|
|
155
|
+
settings.update("broker_host", str(broker_url))
|
|
156
|
+
settings.update("cluster_id", chosen.id)
|
|
157
|
+
settings.save(get_root_cfg().saved_settings_path)
|
|
158
|
+
auth.set_active_cluster(chosen.id, broker_url)
|
|
159
|
+
print_mutation_result(
|
|
160
|
+
{
|
|
161
|
+
"status": "ok",
|
|
162
|
+
"cluster_id": str(chosen.id),
|
|
163
|
+
"cluster_name": chosen.name,
|
|
164
|
+
"cluster_host": str(broker_url),
|
|
165
|
+
},
|
|
166
|
+
Messages.T019.format(noun="cluster", name=chosen.name),
|
|
167
|
+
as_json=as_json,
|
|
168
|
+
)
|
|
169
|
+
return chosen.id
|
|
170
|
+
|
|
171
|
+
|
|
132
172
|
@cli.subcommand(
|
|
133
173
|
"clusters",
|
|
134
174
|
"info",
|
|
@@ -235,12 +275,12 @@ def check(
|
|
|
235
275
|
"""Check the health of a cluster deployment.
|
|
236
276
|
|
|
237
277
|
Runs CLI-side connectivity checks against the cluster and PAM.
|
|
238
|
-
Use --deep
|
|
239
|
-
|
|
278
|
+
Use --deep (or supply --s3-bucket / --nfs-path / --recipe) to also
|
|
279
|
+
trigger cluster-side deployment checks via the broker's /v1/check
|
|
280
|
+
endpoints (K8s API, NFS, S3, recipe execution, DB).
|
|
240
281
|
"""
|
|
241
282
|
auth = get_auth_state()
|
|
242
283
|
results: builtins.list[tuple[str, bool]] = []
|
|
243
|
-
# Resolve the cluster to check
|
|
244
284
|
if name_or_id:
|
|
245
285
|
cluster = resolve_cluster(name_or_id)
|
|
246
286
|
else:
|
|
@@ -252,7 +292,6 @@ def check(
|
|
|
252
292
|
raise SystemExit(1)
|
|
253
293
|
cluster = clusters[0]
|
|
254
294
|
broker_url = URL.parse(cluster.address)
|
|
255
|
-
# 1. Cluster API responding
|
|
256
295
|
try:
|
|
257
296
|
r = httpx.get(f"{broker_url}/api/v1/status", timeout=10)
|
|
258
297
|
r.raise_for_status()
|
|
@@ -260,7 +299,6 @@ def check(
|
|
|
260
299
|
api_ok = payload.get("status") == "Ready"
|
|
261
300
|
cluster_ok = payload.get("cluster") == "Ready"
|
|
262
301
|
results.append(_check_result(f"Cluster API responding on {broker_url}", api_ok))
|
|
263
|
-
# 2. Cluster status (K8s connectivity from cluster)
|
|
264
302
|
if api_ok:
|
|
265
303
|
results.append(
|
|
266
304
|
_check_result(
|
|
@@ -292,6 +330,92 @@ def check(
|
|
|
292
330
|
)
|
|
293
331
|
)
|
|
294
332
|
|
|
333
|
+
if deep or s3_bucket or nfs_path or recipe_name_or_id:
|
|
334
|
+
results.extend(
|
|
335
|
+
_run_deep_checks(
|
|
336
|
+
s3_bucket=s3_bucket,
|
|
337
|
+
nfs_path=nfs_path,
|
|
338
|
+
recipe_name_or_id=recipe_name_or_id,
|
|
339
|
+
recipe_args=recipe_args,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if any(not ok for _, ok in results):
|
|
344
|
+
raise SystemExit(1)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _parse_recipe_args(recipe_args: Optional[str]) -> dict[str, Any]:
|
|
348
|
+
if not recipe_args:
|
|
349
|
+
return {}
|
|
350
|
+
try:
|
|
351
|
+
parsed = json.loads(recipe_args)
|
|
352
|
+
except json.JSONDecodeError as exc:
|
|
353
|
+
msg.fail(f"--recipe-args must be valid JSON: {exc}")
|
|
354
|
+
raise SystemExit(1)
|
|
355
|
+
if not isinstance(parsed, dict):
|
|
356
|
+
msg.fail("--recipe-args must be a JSON object")
|
|
357
|
+
raise SystemExit(1)
|
|
358
|
+
return parsed
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _run_deep_checks(
|
|
362
|
+
*,
|
|
363
|
+
s3_bucket: Optional[str],
|
|
364
|
+
nfs_path: Optional[str],
|
|
365
|
+
recipe_name_or_id: Optional[Union[str, UUID]],
|
|
366
|
+
recipe_args: Optional[str],
|
|
367
|
+
) -> builtins.list[tuple[str, bool]]:
|
|
368
|
+
"""Drive the broker's /v1/check/start + /v1/check/progress polling loop.
|
|
369
|
+
|
|
370
|
+
Returns one (label, ok) tuple per individual sub-check that ran on the
|
|
371
|
+
cluster. "skip" results are reported as ok=True with a "skipped"
|
|
372
|
+
detail — they mean the broker didn't have enough config to run that
|
|
373
|
+
particular check, not that anything failed.
|
|
374
|
+
"""
|
|
375
|
+
auth = get_auth_state()
|
|
376
|
+
recipe_name: Optional[str] = None
|
|
377
|
+
if recipe_name_or_id is not None:
|
|
378
|
+
recipe = resolve_recipe(recipe_name_or_id, cluster_id=None)
|
|
379
|
+
recipe_name = recipe.name
|
|
380
|
+
body = CheckStartRequest(
|
|
381
|
+
s3_bucket=s3_bucket,
|
|
382
|
+
nfs_path=nfs_path,
|
|
383
|
+
recipe_name=recipe_name,
|
|
384
|
+
recipe_args=_parse_recipe_args(recipe_args) if recipe_args else None,
|
|
385
|
+
package_environment=None,
|
|
386
|
+
)
|
|
387
|
+
msg.info("Starting cluster-side deployment checks...")
|
|
388
|
+
started = auth.broker_client.check.start(body)
|
|
389
|
+
deadline = time.time() + 300
|
|
390
|
+
poll_interval = 2.0
|
|
391
|
+
report: Optional[dict[str, str]] = None
|
|
392
|
+
while time.time() < deadline:
|
|
393
|
+
progress = auth.broker_client.check.progress(
|
|
394
|
+
CheckProgressRequest(id=started.id)
|
|
395
|
+
)
|
|
396
|
+
if progress.status.value == "done":
|
|
397
|
+
report = progress.report if isinstance(progress.report, dict) else None
|
|
398
|
+
break
|
|
399
|
+
time.sleep(poll_interval)
|
|
400
|
+
if report is None:
|
|
401
|
+
return [
|
|
402
|
+
_check_result(
|
|
403
|
+
"Cluster-side deployment checks",
|
|
404
|
+
False,
|
|
405
|
+
detail="timed out waiting for broker to finish",
|
|
406
|
+
)
|
|
407
|
+
]
|
|
408
|
+
rows: builtins.list[tuple[str, bool]] = []
|
|
409
|
+
for name, outcome in sorted(report.items()):
|
|
410
|
+
label = f"cluster: {name}"
|
|
411
|
+
if outcome == "success":
|
|
412
|
+
rows.append(_check_result(label, True))
|
|
413
|
+
elif outcome == "skip":
|
|
414
|
+
rows.append(_check_result(label, True, detail="skipped"))
|
|
415
|
+
else:
|
|
416
|
+
rows.append(_check_result(label, False, detail=str(outcome)))
|
|
417
|
+
return rows
|
|
418
|
+
|
|
295
419
|
|
|
296
420
|
@cli.subcommand(
|
|
297
421
|
"clusters",
|
|
@@ -149,26 +149,6 @@ def agent(
|
|
|
149
149
|
return agent_id
|
|
150
150
|
|
|
151
151
|
|
|
152
|
-
@cli.subcommand(
|
|
153
|
-
"config",
|
|
154
|
-
"set-cluster-host",
|
|
155
|
-
host=Arg(help=Messages.cluster_host_config),
|
|
156
|
-
as_json=Arg("--json", help=Messages.as_json),
|
|
157
|
-
)
|
|
158
|
-
def set_broker_host(host: str, as_json: bool = False) -> None:
|
|
159
|
-
"""Set the cluster cluster host."""
|
|
160
|
-
root_cfg = get_root_cfg()
|
|
161
|
-
host_url = URL.parse(host)
|
|
162
|
-
settings = get_saved_settings()
|
|
163
|
-
settings.update("broker_host", str(host_url))
|
|
164
|
-
settings.save(root_cfg.saved_settings_path)
|
|
165
|
-
print_mutation_result(
|
|
166
|
-
{"status": "ok", "cluster_host": str(host_url)},
|
|
167
|
-
Messages.T019.format(noun="cluster host", name=host),
|
|
168
|
-
as_json=as_json,
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
|
|
172
152
|
@cli.subcommand(
|
|
173
153
|
"config",
|
|
174
154
|
"set-pam-host",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import shutil
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any, Dict, List, Literal, Optional
|
|
4
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
5
|
+
from uuid import UUID
|
|
5
6
|
|
|
6
7
|
import httpx
|
|
7
8
|
from radicli import Arg
|
|
@@ -14,7 +15,9 @@ from ..config import SavedSettings, global_config_dir
|
|
|
14
15
|
from ..errors import CLIError, EllfError
|
|
15
16
|
from ..messages import Messages
|
|
16
17
|
from ..ui import print_as_json, print_mutation_result
|
|
17
|
-
from
|
|
18
|
+
from ..util import URL
|
|
19
|
+
from ._cluster_select import select_cluster
|
|
20
|
+
from ._state import get_auth_state, get_root_cfg, get_saved_settings
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
def _ellf_claude_plugin_dir() -> Path | None:
|
|
@@ -60,12 +63,14 @@ def _install_claude_skills() -> None:
|
|
|
60
63
|
"login",
|
|
61
64
|
no_cluster=Arg("--no-cluster", help=Messages.no_cluster),
|
|
62
65
|
no_browser=Arg("--no-browser", help=Messages.no_browser),
|
|
66
|
+
cluster=Arg("--cluster", help=Messages.login_cluster),
|
|
63
67
|
as_json=Arg("--json", help=Messages.as_json),
|
|
64
68
|
claude=Arg("--claude", help=Messages.claude),
|
|
65
69
|
)
|
|
66
70
|
def login(
|
|
67
71
|
no_cluster: bool = False,
|
|
68
72
|
no_browser: bool = False,
|
|
73
|
+
cluster: Optional[Union[str, UUID]] = None,
|
|
69
74
|
as_json: bool = False,
|
|
70
75
|
claude: bool = False,
|
|
71
76
|
) -> None:
|
|
@@ -79,6 +84,7 @@ def login(
|
|
|
79
84
|
auth.get_api_token(force_refresh=True)
|
|
80
85
|
if not no_cluster:
|
|
81
86
|
try:
|
|
87
|
+
_select_and_persist_cluster(auth, cluster)
|
|
82
88
|
auth.get_cluster_token(force_refresh=True)
|
|
83
89
|
except EllfError as e:
|
|
84
90
|
err = Messages.E116.format(command=f"{cli.prog} login --no-cluster")
|
|
@@ -92,6 +98,25 @@ def login(
|
|
|
92
98
|
_install_claude_skills()
|
|
93
99
|
|
|
94
100
|
|
|
101
|
+
def _select_and_persist_cluster(
|
|
102
|
+
auth: AuthState,
|
|
103
|
+
name_or_id: Optional[Union[str, UUID]] = None,
|
|
104
|
+
) -> UUID:
|
|
105
|
+
"""Pick a cluster via pam and write it into SavedSettings.
|
|
106
|
+
|
|
107
|
+
Mutates ``auth`` so subsequent ``get_cluster_token`` / ``broker_client``
|
|
108
|
+
calls in the same process use the chosen cluster without re-querying.
|
|
109
|
+
"""
|
|
110
|
+
chosen = select_cluster(auth.client, name_or_id)
|
|
111
|
+
broker_url = URL.parse(chosen.address)
|
|
112
|
+
settings = get_saved_settings()
|
|
113
|
+
settings.update("broker_host", str(broker_url))
|
|
114
|
+
settings.update("cluster_id", chosen.id)
|
|
115
|
+
settings.save(get_root_cfg().saved_settings_path)
|
|
116
|
+
auth.set_active_cluster(chosen.id, broker_url)
|
|
117
|
+
return chosen.id
|
|
118
|
+
|
|
119
|
+
|
|
95
120
|
@cli.command("info", field=Arg(help=Messages.select_field))
|
|
96
121
|
def info(field: Optional[Literal["config-dir", "code", "defaults"]] = None) -> Any:
|
|
97
122
|
"""Print information about the CLI"""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"prog": "ellf",
|
|
3
3
|
"help": "Ellf Command Line Interface.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.0.3",
|
|
5
5
|
"extra_key": "_extra",
|
|
6
6
|
"commands": {
|
|
7
7
|
"actions": {
|
|
@@ -177,6 +177,19 @@
|
|
|
177
177
|
"type": null,
|
|
178
178
|
"orig_type": "bool"
|
|
179
179
|
},
|
|
180
|
+
{
|
|
181
|
+
"id": "cluster",
|
|
182
|
+
"option": "--cluster",
|
|
183
|
+
"short": null,
|
|
184
|
+
"orig_help": "Name or ID of the cluster to log into. If omitted: auto-select when only one is available, or prompt interactively",
|
|
185
|
+
"default": null,
|
|
186
|
+
"help": "Name or ID of the cluster to log into. If omitted: auto-select when only one is available, or prompt interactively (str)",
|
|
187
|
+
"action": null,
|
|
188
|
+
"choices": null,
|
|
189
|
+
"has_converter": false,
|
|
190
|
+
"type": "str",
|
|
191
|
+
"orig_type": "str"
|
|
192
|
+
},
|
|
180
193
|
{
|
|
181
194
|
"id": "as_json",
|
|
182
195
|
"option": "--json",
|
|
@@ -1777,6 +1790,41 @@
|
|
|
1777
1790
|
"parent": "clusters",
|
|
1778
1791
|
"is_placeholder": false
|
|
1779
1792
|
},
|
|
1793
|
+
"use": {
|
|
1794
|
+
"name": "use",
|
|
1795
|
+
"args": [
|
|
1796
|
+
{
|
|
1797
|
+
"id": "name_or_id",
|
|
1798
|
+
"option": null,
|
|
1799
|
+
"short": null,
|
|
1800
|
+
"orig_help": "Name or ID of the cluster",
|
|
1801
|
+
"default": null,
|
|
1802
|
+
"help": "Name or ID of the cluster (str)",
|
|
1803
|
+
"action": null,
|
|
1804
|
+
"choices": null,
|
|
1805
|
+
"has_converter": false,
|
|
1806
|
+
"type": "str",
|
|
1807
|
+
"orig_type": "str"
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
"id": "as_json",
|
|
1811
|
+
"option": "--json",
|
|
1812
|
+
"short": null,
|
|
1813
|
+
"orig_help": "Output the result as JSON",
|
|
1814
|
+
"default": false,
|
|
1815
|
+
"help": "Output the result as JSON (bool)",
|
|
1816
|
+
"action": "store_true",
|
|
1817
|
+
"choices": null,
|
|
1818
|
+
"has_converter": false,
|
|
1819
|
+
"type": null,
|
|
1820
|
+
"orig_type": "bool"
|
|
1821
|
+
}
|
|
1822
|
+
],
|
|
1823
|
+
"description": "Switch the active cluster.\n\nLooks up the cluster by name or ID via PAM. With no argument and\nmultiple clusters available, prompts interactively in a TTY.\n",
|
|
1824
|
+
"allow_extra": false,
|
|
1825
|
+
"parent": "clusters",
|
|
1826
|
+
"is_placeholder": false
|
|
1827
|
+
},
|
|
1780
1828
|
"info": {
|
|
1781
1829
|
"name": "info",
|
|
1782
1830
|
"args": [
|
|
@@ -1990,7 +2038,7 @@
|
|
|
1990
2038
|
"orig_type": "bool"
|
|
1991
2039
|
}
|
|
1992
2040
|
],
|
|
1993
|
-
"description": "Check the health of a cluster deployment.\n\nRuns CLI-side connectivity checks against the cluster and PAM.\nUse --deep to also
|
|
2041
|
+
"description": "Check the health of a cluster deployment.\n\nRuns CLI-side connectivity checks against the cluster and PAM.\nUse --deep (or supply --s3-bucket / --nfs-path / --recipe) to also\ntrigger cluster-side deployment checks via the broker's /v1/check\nendpoints (K8s API, NFS, S3, recipe execution, DB).\n",
|
|
1994
2042
|
"allow_extra": false,
|
|
1995
2043
|
"parent": "clusters",
|
|
1996
2044
|
"is_placeholder": false
|
|
@@ -2490,41 +2538,6 @@
|
|
|
2490
2538
|
"parent": "config",
|
|
2491
2539
|
"is_placeholder": false
|
|
2492
2540
|
},
|
|
2493
|
-
"set-cluster-host": {
|
|
2494
|
-
"name": "set-cluster-host",
|
|
2495
|
-
"args": [
|
|
2496
|
-
{
|
|
2497
|
-
"id": "host",
|
|
2498
|
-
"option": null,
|
|
2499
|
-
"short": null,
|
|
2500
|
-
"orig_help": "Host or URL of the cluster",
|
|
2501
|
-
"default": "==SUPPRESS==",
|
|
2502
|
-
"help": "Host or URL of the cluster (str)",
|
|
2503
|
-
"action": null,
|
|
2504
|
-
"choices": null,
|
|
2505
|
-
"has_converter": false,
|
|
2506
|
-
"type": "str",
|
|
2507
|
-
"orig_type": "str"
|
|
2508
|
-
},
|
|
2509
|
-
{
|
|
2510
|
-
"id": "as_json",
|
|
2511
|
-
"option": "--json",
|
|
2512
|
-
"short": null,
|
|
2513
|
-
"orig_help": "Output the result as JSON",
|
|
2514
|
-
"default": false,
|
|
2515
|
-
"help": "Output the result as JSON (bool)",
|
|
2516
|
-
"action": "store_true",
|
|
2517
|
-
"choices": null,
|
|
2518
|
-
"has_converter": false,
|
|
2519
|
-
"type": null,
|
|
2520
|
-
"orig_type": "bool"
|
|
2521
|
-
}
|
|
2522
|
-
],
|
|
2523
|
-
"description": "Set the cluster cluster host.",
|
|
2524
|
-
"allow_extra": false,
|
|
2525
|
-
"parent": "config",
|
|
2526
|
-
"is_placeholder": false
|
|
2527
|
-
},
|
|
2528
2541
|
"set-pam-host": {
|
|
2529
2542
|
"name": "set-pam-host",
|
|
2530
2543
|
"args": [
|
|
@@ -36,7 +36,7 @@ class Messages:
|
|
|
36
36
|
E032 = "Login failed: the device authentication API sent an incomplete response"
|
|
37
37
|
E033 = "Login failed: unable to load CLI config from {url}"
|
|
38
38
|
E034 = "{code} {reason} for URL {url}"
|
|
39
|
-
E035 = "
|
|
39
|
+
E035 = "No active cluster: run `ellf login` to pick one (or `ellf clusters use <name>` to switch)"
|
|
40
40
|
E036 = "Cluster ID unset"
|
|
41
41
|
E038 = "Could not find {noun}: {name_or_id}"
|
|
42
42
|
E039 = "Authentication error: {message}"
|
|
@@ -168,10 +168,10 @@ ellf secret create my-credentials OPENAI_API_KEY="sk-..." CUSTOM_SECRET=-
|
|
|
168
168
|
asset_kind = "Kind of the asset. Generally one of: ['Input', 'Model', 'Patterns]"
|
|
169
169
|
asset_meta = "Asset meta, formatted as a JSON string"
|
|
170
170
|
output_dir = "Output directory for the {noun}"
|
|
171
|
-
cluster_host_config = "Host or URL of the cluster"
|
|
172
171
|
pam_host_config = "Host or URL of the Prodigy Annotation Manager (PAM) app"
|
|
173
172
|
no_cluster = "Don't use a cluster"
|
|
174
173
|
no_browser = "Don't open a browser, just print the login URL"
|
|
174
|
+
login_cluster = "Name or ID of the cluster to log into. If omitted: auto-select when only one is available, or prompt interactively"
|
|
175
175
|
claude = "Install Ellf Claude Code skills and transcript hook into ~/.claude/"
|
|
176
176
|
use_active_venv = "Use the currently active virtualenv, instead of making a temporary one"
|
|
177
177
|
filter_by = "Filter by {filter}"
|
|
@@ -211,7 +211,7 @@ ellf secret create my-credentials OPENAI_API_KEY="sk-..." CUSTOM_SECRET=-
|
|
|
211
211
|
E104 = "Error authenticating device and user with Ellf"
|
|
212
212
|
E105 = "Error communicating with Ellf API"
|
|
213
213
|
E106 = "No clusters registered"
|
|
214
|
-
E107 = "Multiple clusters
|
|
214
|
+
E107 = "Multiple clusters available. Run `ellf login --cluster <name>` (or `ellf clusters use <name>`) to pick one"
|
|
215
215
|
E116 = "Could not login to cluster. Run {command} to log in to without connecting to a cluster"
|
|
216
216
|
E117 = "Unrecognized token type {token_type!r}"
|
|
217
217
|
E118 = "Error requesting recipe schemas"
|
|
@@ -34,6 +34,7 @@ ellf_cli.egg-info/top_level.txt
|
|
|
34
34
|
ellf_cli/cloud/__init__.py
|
|
35
35
|
ellf_cli/cloud/gcp.py
|
|
36
36
|
ellf_cli/commands/__init__.py
|
|
37
|
+
ellf_cli/commands/_cluster_select.py
|
|
37
38
|
ellf_cli/commands/_recipe_file.py
|
|
38
39
|
ellf_cli/commands/_recipe_subcommand.py
|
|
39
40
|
ellf_cli/commands/_state.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|