vidbyte-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vidbyte_cli/__init__.py +11 -0
- vidbyte_cli/__main__.py +6 -0
- vidbyte_cli/cli.py +18 -0
- vidbyte_cli/commands/__init__.py +86 -0
- vidbyte_cli/commands/agent_options.py +53 -0
- vidbyte_cli/commands/agents/__init__.py +31 -0
- vidbyte_cli/commands/agents/suggestion/__init__.py +38 -0
- vidbyte_cli/commands/agents/suggestion/prompts/__init__.py +3 -0
- vidbyte_cli/commands/agents/suggestion/prompts/all_categories.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/approaches.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/avoid.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/blockers.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/capability.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/categories.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/category.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/completed.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/constraint.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/context.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/count.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/critic_model.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/decision.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/dry_run.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/extra_compute.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_accept.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_group.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_project.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_reason.md +98 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_reject.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_suggestion.md +101 -0
- vidbyte_cli/commands/agents/suggestion/prompts/files.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/forbidden.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/goal.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/handoff.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/horizon.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/hypotheses.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/idea.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/in_progress.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/input.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/library.py +29 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_messages.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_output_tokens.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_total_tokens.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/mistakes.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/outcomes.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_create.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_description.md +98 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_group.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_key.md +96 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_list.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_title.md +99 -0
- vidbyte_cli/commands/agents/suggestion/prompts/provider.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/question.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/risks.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/rounds.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/run.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/success.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/timeout.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/trajectory.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/view.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/view_all.md +5 -0
- vidbyte_cli/commands/agents/suggestion/render.py +77 -0
- vidbyte_cli/commands/agents/suggestion/request_builder.py +240 -0
- vidbyte_cli/commands/agents/suggestion/suggest.py +154 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_categories.py +77 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_feedback.py +85 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_handoff.py +67 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_projects.py +95 -0
- vidbyte_cli/commands/auth/__init__.py +0 -0
- vidbyte_cli/commands/auth/login.py +100 -0
- vidbyte_cli/commands/auth/logout.py +41 -0
- vidbyte_cli/commands/auth/whoami.py +51 -0
- vidbyte_cli/commands/billing/__init__.py +20 -0
- vidbyte_cli/commands/billing/prompts/__init__.py +1 -0
- vidbyte_cli/commands/billing/prompts/confirm.md +16 -0
- vidbyte_cli/commands/billing/prompts/group.md +13 -0
- vidbyte_cli/commands/billing/prompts/idempotency_key.md +16 -0
- vidbyte_cli/commands/billing/prompts/library.py +18 -0
- vidbyte_cli/commands/billing/prompts/top_up.md +16 -0
- vidbyte_cli/commands/billing/top_up.py +65 -0
- vidbyte_cli/commands/config/__init__.py +0 -0
- vidbyte_cli/commands/config/get.py +45 -0
- vidbyte_cli/commands/config/set.py +39 -0
- vidbyte_cli/commands/provider/__init__.py +9 -0
- vidbyte_cli/commands/provider/login.py +102 -0
- vidbyte_cli/commands/provider/logout.py +45 -0
- vidbyte_cli/commands/provider/whoami.py +49 -0
- vidbyte_cli/commands/research/__init__.py +5 -0
- vidbyte_cli/commands/research/add.py +41 -0
- vidbyte_cli/commands/research/options.py +134 -0
- vidbyte_cli/commands/research/render.py +157 -0
- vidbyte_cli/commands/research/resume.py +37 -0
- vidbyte_cli/commands/research/start.py +37 -0
- vidbyte_cli/commands/research/status.py +32 -0
- vidbyte_cli/commands/research/thread.py +30 -0
- vidbyte_cli/commands/research/threads.py +35 -0
- vidbyte_cli/commands/research/watch.py +93 -0
- vidbyte_cli/commands/runtime/__init__.py +23 -0
- vidbyte_cli/commands/runtime/adversarial_team.py +41 -0
- vidbyte_cli/commands/runtime/doctor.py +44 -0
- vidbyte_cli/commands/runtime/list.py +47 -0
- vidbyte_cli/commands/runtime/persistence.py +141 -0
- vidbyte_cli/commands/runtime/same_host_ensemble.py +221 -0
- vidbyte_cli/commands/runtime/stages.py +553 -0
- vidbyte_cli/commands/runtime/task_board.py +1342 -0
- vidbyte_cli/commands/setup/__init__.py +0 -0
- vidbyte_cli/commands/setup/doctor.py +64 -0
- vidbyte_cli/commands/setup/prompts/__init__.py +1 -0
- vidbyte_cli/commands/setup/prompts/browser.md +7 -0
- vidbyte_cli/commands/setup/prompts/library.py +18 -0
- vidbyte_cli/commands/setup/prompts/star.md +13 -0
- vidbyte_cli/commands/setup/star.py +132 -0
- vidbyte_cli/lib/__init__.py +0 -0
- vidbyte_cli/lib/api/__init__.py +0 -0
- vidbyte_cli/lib/api/client.py +236 -0
- vidbyte_cli/lib/api/endpoints/__init__.py +0 -0
- vidbyte_cli/lib/api/endpoints/auth.py +24 -0
- vidbyte_cli/lib/api/endpoints/billing.py +36 -0
- vidbyte_cli/lib/api/endpoints/research.py +103 -0
- vidbyte_cli/lib/api/endpoints/runtime.py +113 -0
- vidbyte_cli/lib/api/problem.py +86 -0
- vidbyte_cli/lib/api/response.py +101 -0
- vidbyte_cli/lib/api/retry.py +116 -0
- vidbyte_cli/lib/api/runtime_payment.py +89 -0
- vidbyte_cli/lib/auth/__init__.py +16 -0
- vidbyte_cli/lib/auth/credentials.py +223 -0
- vidbyte_cli/lib/auth/input.py +59 -0
- vidbyte_cli/lib/auth/keyring_store.py +126 -0
- vidbyte_cli/lib/auth/provider_credentials.py +157 -0
- vidbyte_cli/lib/auth/provider_input.py +54 -0
- vidbyte_cli/lib/auth/provider_resolver.py +89 -0
- vidbyte_cli/lib/auth/provider_store.py +149 -0
- vidbyte_cli/lib/auth/provider_verifier.py +219 -0
- vidbyte_cli/lib/auth/resolver.py +63 -0
- vidbyte_cli/lib/auth/verifier.py +38 -0
- vidbyte_cli/lib/config/__init__.py +25 -0
- vidbyte_cli/lib/config/atomic.py +47 -0
- vidbyte_cli/lib/config/config.py +141 -0
- vidbyte_cli/lib/config/migration.py +81 -0
- vidbyte_cli/lib/config/models.py +145 -0
- vidbyte_cli/lib/config/paths.py +66 -0
- vidbyte_cli/lib/config/resolver.py +137 -0
- vidbyte_cli/lib/constants/__init__.py +1 -0
- vidbyte_cli/lib/constants/runtime.py +186 -0
- vidbyte_cli/lib/errors/__init__.py +12 -0
- vidbyte_cli/lib/errors/cli_error.py +86 -0
- vidbyte_cli/lib/errors/codes.py +43 -0
- vidbyte_cli/lib/errors/failures.py +2910 -0
- vidbyte_cli/lib/errors/handler.py +72 -0
- vidbyte_cli/lib/files/__init__.py +6 -0
- vidbyte_cli/lib/files/documents.py +94 -0
- vidbyte_cli/lib/files/store.py +156 -0
- vidbyte_cli/lib/io/__init__.py +16 -0
- vidbyte_cli/lib/io/attachments.py +129 -0
- vidbyte_cli/lib/io/codex_attachments.py +47 -0
- vidbyte_cli/lib/io/prompt.py +71 -0
- vidbyte_cli/lib/io/streams.py +37 -0
- vidbyte_cli/lib/io/terminal.py +66 -0
- vidbyte_cli/lib/output/__init__.py +7 -0
- vidbyte_cli/lib/output/formats.py +29 -0
- vidbyte_cli/lib/output/manager.py +95 -0
- vidbyte_cli/lib/output/models.py +49 -0
- vidbyte_cli/lib/runtime/__init__.py +9 -0
- vidbyte_cli/lib/runtime/application.py +134 -0
- vidbyte_cli/lib/runtime/context.py +250 -0
- vidbyte_cli/lib/runtime/options.py +159 -0
- vidbyte_cli/lib/runtime/version.py +30 -0
- vidbyte_cli/lib/runtime_primitives/__init__.py +28 -0
- vidbyte_cli/lib/runtime_primitives/executor.py +54 -0
- vidbyte_cli/lib/runtime_primitives/gate.py +105 -0
- vidbyte_cli/lib/runtime_primitives/hosts.py +45 -0
- vidbyte_cli/lib/runtime_primitives/planner.py +79 -0
- vidbyte_cli/lib/runtime_primitives/stages.py +338 -0
- vidbyte_cli/lib/runtime_primitives/task_board.py +982 -0
- vidbyte_cli/lib/runtime_primitives/task_board_checkpoints.py +473 -0
- vidbyte_cli/lib/runtime_primitives/task_board_system.md +33 -0
- vidbyte_cli/lib/runtime_primitives/task_board_tasks.py +111 -0
- vidbyte_cli/lib/runtime_primitives/verification.py +68 -0
- vidbyte_cli/services/__init__.py +5 -0
- vidbyte_cli/services/ensemble/__init__.py +7 -0
- vidbyte_cli/services/ensemble/prompts/__init__.py +6 -0
- vidbyte_cli/services/ensemble/prompts/implementer_system.md +546 -0
- vidbyte_cli/services/ensemble/prompts/implementer_turn.md +49 -0
- vidbyte_cli/services/ensemble/prompts/library.py +188 -0
- vidbyte_cli/services/ensemble/prompts/planner_system.md +728 -0
- vidbyte_cli/services/ensemble/prompts/planner_turn.md +6 -0
- vidbyte_cli/services/ensemble/prompts/role_system.md +104 -0
- vidbyte_cli/services/ensemble/prompts/role_turn.md +19 -0
- vidbyte_cli/services/ensemble/prompts/selector_final_turn.md +19 -0
- vidbyte_cli/services/ensemble/prompts/selector_round_turn.md +17 -0
- vidbyte_cli/services/ensemble/prompts/selector_system.md +89 -0
- vidbyte_cli/services/ensemble/runner.py +30 -0
- vidbyte_cli/services/ensemble/sdk.py +134 -0
- vidbyte_cli/services/ensemble/service.py +233 -0
- vidbyte_cli/services/ensemble/settings.py +60 -0
- vidbyte_cli/services/persistence/__init__.py +6 -0
- vidbyte_cli/services/persistence/prompts/__init__.py +6 -0
- vidbyte_cli/services/persistence/prompts/library.py +46 -0
- vidbyte_cli/services/persistence/prompts/persistence_system.md +1 -0
- vidbyte_cli/services/persistence/prompts/persistence_turn.md +3 -0
- vidbyte_cli/services/persistence/runner.py +35 -0
- vidbyte_cli/services/persistence/session.py +116 -0
- vidbyte_cli/services/suggestions/__init__.py +7 -0
- vidbyte_cli/services/suggestions/categories.py +270 -0
- vidbyte_cli/services/suggestions/context.py +222 -0
- vidbyte_cli/services/suggestions/extra_compute.py +53 -0
- vidbyte_cli/services/suggestions/handoff.py +173 -0
- vidbyte_cli/services/suggestions/message_tools.py +47 -0
- vidbyte_cli/services/suggestions/project.py +253 -0
- vidbyte_cli/services/suggestions/prompts/__init__.py +1 -0
- vidbyte_cli/services/suggestions/prompts/categories/__init__.py +3 -0
- vidbyte_cli/services/suggestions/prompts/categories/adjacent_opportunity.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/alternative.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/big_bets.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/bottleneck.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/business_growth.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/completion.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/continuation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/coordination.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/creative_exploration.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/cross_domain.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/delegation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/experiment.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/feedback.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/goal_clarification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/immediate_next_steps.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/investigation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/learning.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/leverage.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/long_term_directions.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/preparation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/prerequisite.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/prioritization.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/product_experience.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/quick_wins.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/reframing.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/risk_prevention.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/simplification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/stop_or_defer.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/strategy.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/technical_possibilities.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/verification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/critic.md +209 -0
- vidbyte_cli/services/suggestions/prompts/critic_message.md +8 -0
- vidbyte_cli/services/suggestions/prompts/generator.md +64 -0
- vidbyte_cli/services/suggestions/prompts/library.py +72 -0
- vidbyte_cli/services/suggestions/prompts/message_generator_argument.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_generator_tool.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_parent_argument.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_parent_tool.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_receipt.md +1 -0
- vidbyte_cli/services/suggestions/prompts/revision.md +8 -0
- vidbyte_cli/services/suggestions/sdk.py +228 -0
- vidbyte_cli/services/suggestions/selection.py +78 -0
- vidbyte_cli/services/suggestions/service.py +540 -0
- vidbyte_cli/types/__init__.py +0 -0
- vidbyte_cli/types/api.py +101 -0
- vidbyte_cli/types/attachments.py +79 -0
- vidbyte_cli/types/ensemble.py +189 -0
- vidbyte_cli/types/provider.py +62 -0
- vidbyte_cli/types/research.py +260 -0
- vidbyte_cli/types/runtime.py +1098 -0
- vidbyte_cli/types/suggestions.py +518 -0
- vidbyte_cli-0.1.0.dist-info/METADATA +335 -0
- vidbyte_cli-0.1.0.dist-info/RECORD +269 -0
- vidbyte_cli-0.1.0.dist-info/WHEEL +5 -0
- vidbyte_cli-0.1.0.dist-info/entry_points.txt +2 -0
- vidbyte_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- vidbyte_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""What this invocation's terminal can safely be asked to do.
|
|
2
|
+
|
|
3
|
+
Capabilities are data, detected once from injected streams and an injected environment.
|
|
4
|
+
Injecting both is what makes redirection, CI, embedding, and Windows consoles predictable
|
|
5
|
+
instead of depending on process globals. No escape sequence is ever written from here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import TextIO
|
|
14
|
+
|
|
15
|
+
from ..output.formats import ColorMode
|
|
16
|
+
from .streams import IOStreams
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class TerminalPolicy:
|
|
21
|
+
"""Preferences and environment used for one capability decision."""
|
|
22
|
+
|
|
23
|
+
color: ColorMode = ColorMode.AUTO
|
|
24
|
+
no_input: bool = False
|
|
25
|
+
environment: Mapping[str, str] | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class TerminalCapabilities:
|
|
30
|
+
"""Terminal features safe to use for one invocation."""
|
|
31
|
+
|
|
32
|
+
interactive: bool
|
|
33
|
+
color: bool
|
|
34
|
+
cursor: bool
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def detect(cls, streams: IOStreams, policy: TerminalPolicy) -> TerminalCapabilities:
|
|
38
|
+
# Environment injection avoids hiding global reads inside renderers.
|
|
39
|
+
environment = os.environ if policy.environment is None else policy.environment
|
|
40
|
+
terminal_allowed = cls._terminal_control_allowed(cls._is_tty(streams.stderr), environment)
|
|
41
|
+
return cls(
|
|
42
|
+
interactive=cls._is_tty(streams.stdin) and not policy.no_input,
|
|
43
|
+
color=terminal_allowed and policy.color is not ColorMode.NEVER,
|
|
44
|
+
cursor=terminal_allowed,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _is_tty(stream: TextIO) -> bool:
|
|
49
|
+
# Redirected and in-memory streams may expose no usable isatty implementation.
|
|
50
|
+
isatty = getattr(stream, "isatty", None)
|
|
51
|
+
if not callable(isatty):
|
|
52
|
+
return False
|
|
53
|
+
try:
|
|
54
|
+
return bool(isatty())
|
|
55
|
+
except (OSError, ValueError):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _terminal_control_allowed(stderr_tty: bool, environment: Mapping[str, str]) -> bool:
|
|
60
|
+
# Accessibility and shell safety outrank an explicit color request: even
|
|
61
|
+
# `--color always` cannot turn on control sequences here.
|
|
62
|
+
if not stderr_tty:
|
|
63
|
+
return False
|
|
64
|
+
if environment.get("TERM", "").lower() == "dumb":
|
|
65
|
+
return False
|
|
66
|
+
return "NO_COLOR" not in environment
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Public presentation contracts. Feature presenters stay private to their own slices."""
|
|
2
|
+
|
|
3
|
+
from .formats import ColorMode, OutputFormat
|
|
4
|
+
from .manager import OutputManager, OutputPolicy
|
|
5
|
+
from .models import OutputDocument
|
|
6
|
+
|
|
7
|
+
__all__ = ["ColorMode", "OutputDocument", "OutputFormat", "OutputManager", "OutputPolicy"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The root presentation vocabulary shared by flags, terminal detection, and rendering.
|
|
2
|
+
|
|
3
|
+
String enums keep Click choices, config values, and diagnostics on one set of spellings.
|
|
4
|
+
Nothing here inspects a TTY or serializes a document — `lib/io` owns capability detection
|
|
5
|
+
and `manager.py` owns stream policy.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OutputFormat(StrEnum):
|
|
14
|
+
"""Supported result serialization modes."""
|
|
15
|
+
|
|
16
|
+
HUMAN = "human"
|
|
17
|
+
JSON = "json"
|
|
18
|
+
JSONL = "jsonl"
|
|
19
|
+
NONE = "none"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ColorMode(StrEnum):
|
|
23
|
+
"""User preference for terminal color when the stream permits it."""
|
|
24
|
+
|
|
25
|
+
# ALWAYS is still subject to the accessibility rule: a redirected stream, TERM=dumb, or
|
|
26
|
+
# NO_COLOR disables terminal control regardless of what was requested.
|
|
27
|
+
AUTO = "auto"
|
|
28
|
+
ALWAYS = "always"
|
|
29
|
+
NEVER = "never"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Where results, progress, warnings, and failures are allowed to go, for one invocation.
|
|
2
|
+
|
|
3
|
+
Two rules make this file worth existing. Stdout carries results only, so a shell pipeline
|
|
4
|
+
stays parseable while progress and diagnostics run on stderr. And a machine format never
|
|
5
|
+
silently degrades to human text — a consumer that asked for JSON gets JSON or an error.
|
|
6
|
+
|
|
7
|
+
Human error text repeats the agent-native description/trace/file_path that machine documents
|
|
8
|
+
carry, because agents shell out to the default format far more often than they pass --json.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import TYPE_CHECKING, Protocol
|
|
15
|
+
|
|
16
|
+
from .formats import OutputFormat
|
|
17
|
+
from .models import OutputDocument
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..errors.cli_error import CliError
|
|
21
|
+
from ..io.terminal import TerminalCapabilities
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OutputStreams(Protocol):
|
|
25
|
+
"""Minimum physical stream behavior required by output policy."""
|
|
26
|
+
|
|
27
|
+
def write_output(self, message: str) -> None: ...
|
|
28
|
+
|
|
29
|
+
def write_error(self, message: str) -> None: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class OutputPolicy:
|
|
34
|
+
"""Presentation choices used by one output manager."""
|
|
35
|
+
|
|
36
|
+
output_format: OutputFormat
|
|
37
|
+
terminal: TerminalCapabilities
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OutputManager:
|
|
41
|
+
"""Invocation-owned presentation and stream policy."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, streams: OutputStreams, policy: OutputPolicy) -> None:
|
|
44
|
+
self._streams = streams
|
|
45
|
+
self.format = policy.output_format
|
|
46
|
+
self.terminal = policy.terminal
|
|
47
|
+
|
|
48
|
+
def result(self, document: OutputDocument, human: str) -> None:
|
|
49
|
+
# JSON and JSONL both emit one record here; only JSONL may emit earlier transitions.
|
|
50
|
+
if self.format is OutputFormat.NONE:
|
|
51
|
+
return
|
|
52
|
+
if self.format is OutputFormat.HUMAN:
|
|
53
|
+
self._streams.write_output(human)
|
|
54
|
+
return
|
|
55
|
+
self._streams.write_output(self._serialize(document))
|
|
56
|
+
|
|
57
|
+
def transition(self, document: OutputDocument, human: str) -> None:
|
|
58
|
+
# A single-JSON consumer must see exactly one final stdout document.
|
|
59
|
+
if self.format is OutputFormat.JSONL:
|
|
60
|
+
self._streams.write_output(self._serialize(document))
|
|
61
|
+
elif self.format is OutputFormat.HUMAN:
|
|
62
|
+
self._streams.write_error(human)
|
|
63
|
+
|
|
64
|
+
def warning(self, message: str) -> None:
|
|
65
|
+
# Warnings stay visible in every format without corrupting structured stdout.
|
|
66
|
+
self._streams.write_error(f"Warning: {message}")
|
|
67
|
+
|
|
68
|
+
def diagnostic(self, message: str) -> None:
|
|
69
|
+
# Debug and verbose details are diagnostics, never command results.
|
|
70
|
+
self._streams.write_error(message)
|
|
71
|
+
|
|
72
|
+
def error(self, error: CliError) -> None:
|
|
73
|
+
# NONE suppresses ordinary output, not actionable failures.
|
|
74
|
+
if self.format in {OutputFormat.JSON, OutputFormat.JSONL}:
|
|
75
|
+
self._streams.write_error(self._serialize(OutputDocument.from_error(error)))
|
|
76
|
+
return
|
|
77
|
+
self._streams.write_error(self._human_error(error))
|
|
78
|
+
|
|
79
|
+
def _serialize(self, document: OutputDocument) -> str:
|
|
80
|
+
# Pydantic owns strict JSON encoding; failures propagate as internal software errors.
|
|
81
|
+
return document.model_dump_json(exclude_none=True)
|
|
82
|
+
|
|
83
|
+
def _human_error(self, error: CliError) -> str:
|
|
84
|
+
# Text labels preserve meaning when color and cursor control are unavailable.
|
|
85
|
+
lines = [
|
|
86
|
+
f"Error [{error.code.value}]: {error.message}",
|
|
87
|
+
f"Details: {error.description}",
|
|
88
|
+
f"Trace: {error.trace}",
|
|
89
|
+
f"Source: {error.file_path}",
|
|
90
|
+
]
|
|
91
|
+
if error.hint is not None:
|
|
92
|
+
lines.append(f"Hint: {error.hint}")
|
|
93
|
+
if error.request_id is not None:
|
|
94
|
+
lines.append(f"Request ID: {error.request_id}")
|
|
95
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""The versioned envelope every machine-readable record travels in.
|
|
2
|
+
|
|
3
|
+
`schema_version` and `kind` are the compatibility contract: a consumer branches on them and
|
|
4
|
+
finds its own keys under `data`. Fields may be added, never removed or repurposed.
|
|
5
|
+
|
|
6
|
+
`CliError` is imported for typing only. A runtime import would cycle, because the errors
|
|
7
|
+
package imports the output manager to render failures.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field, JsonValue
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..errors.cli_error import CliError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OutputDocument(BaseModel):
|
|
21
|
+
"""One stable result, transition, or error record."""
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
24
|
+
|
|
25
|
+
schema_version: Literal[1] = 1
|
|
26
|
+
kind: str = Field(min_length=1, pattern=r"^[a-z][a-z0-9_.-]*$")
|
|
27
|
+
data: dict[str, JsonValue] = Field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_error(cls, error: CliError) -> OutputDocument:
|
|
31
|
+
# Only explicitly safe fields cross this boundary; the private cause never does.
|
|
32
|
+
# Optional fields are omitted rather than emitted as null so a shell consumer can
|
|
33
|
+
# tell absent metadata from a deliberate value.
|
|
34
|
+
data: dict[str, JsonValue] = {
|
|
35
|
+
"code": error.code_value,
|
|
36
|
+
"message": error.message,
|
|
37
|
+
"description": error.description,
|
|
38
|
+
"trace": error.trace,
|
|
39
|
+
"file_path": error.file_path,
|
|
40
|
+
"exit_code": error.exit_code,
|
|
41
|
+
"retryable": error.retryable,
|
|
42
|
+
}
|
|
43
|
+
if error.hint is not None:
|
|
44
|
+
data["hint"] = error.hint
|
|
45
|
+
if error.request_id is not None:
|
|
46
|
+
data["request_id"] = error.request_id
|
|
47
|
+
if error.remediation is not None:
|
|
48
|
+
data["remediation"] = error.remediation
|
|
49
|
+
return cls(kind="error", data=data)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Runtime contracts that are safe to import from the package root.
|
|
2
|
+
|
|
3
|
+
CliApplication and ApplicationContext are intentionally NOT re-exported: importing
|
|
4
|
+
`vidbyte_cli` must not pull in click commands or HTTP models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .version import VersionProvider, current_version
|
|
8
|
+
|
|
9
|
+
__all__ = ["VersionProvider", "current_version"]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""One CLI invocation: read root policy, build the command tree, dispatch.
|
|
2
|
+
|
|
3
|
+
This is the composition root. `run()` returns a status instead of calling `sys.exit`, so the
|
|
4
|
+
application can be embedded in another Python process, and every failure — Click's included —
|
|
5
|
+
leaves through the single ErrorHandler boundary rather than being printed here.
|
|
6
|
+
|
|
7
|
+
Root options are inspected before Click parses, because `--format` and `--debug` decide how a
|
|
8
|
+
parse failure is rendered and Click's own errors leave through that same boundary. They are
|
|
9
|
+
overrides, not the final answer: the resolver layers them over the environment and the
|
|
10
|
+
selected profile. An invocation that exits before running a command skips that resolution
|
|
11
|
+
entirely, so `--help` never reads a file.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from ...commands import register_all_commands
|
|
23
|
+
from ..config import ConfigOverrides
|
|
24
|
+
from ..errors.failures import ConflictingOutputFormat
|
|
25
|
+
from ..io import IOStreams
|
|
26
|
+
from ..output.formats import ColorMode, OutputFormat
|
|
27
|
+
from .context import ApplicationContext, InvocationOptions
|
|
28
|
+
from .options import RootOptionInspector, RootOptionValues
|
|
29
|
+
from .version import current_version
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CliApplication:
|
|
33
|
+
"""Runs one command-line invocation and returns its process status."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, context: ApplicationContext | None = None) -> None:
|
|
36
|
+
# A caller may inject a complete graph; otherwise bind the process streams once.
|
|
37
|
+
self._context = context or ApplicationContext(IOStreams.system())
|
|
38
|
+
self._streams = self._context.streams
|
|
39
|
+
|
|
40
|
+
def run(self, argv: Sequence[str] | None = None) -> int:
|
|
41
|
+
# Build, configure, and dispatch inside one trap so every exit path is a return code.
|
|
42
|
+
arguments = list(sys.argv if argv is None else argv)
|
|
43
|
+
try:
|
|
44
|
+
program = self._build_program()
|
|
45
|
+
register_all_commands(program)
|
|
46
|
+
self._preconfigure(arguments)
|
|
47
|
+
return self._invoke(program, arguments)
|
|
48
|
+
except (Exception, KeyboardInterrupt) as error: # noqa: BLE001 - the process boundary.
|
|
49
|
+
return self._context.error_handler().handle(error)
|
|
50
|
+
finally:
|
|
51
|
+
# After the handler has rendered, so a failure still reports before teardown.
|
|
52
|
+
self._context.close()
|
|
53
|
+
|
|
54
|
+
def _build_program(self) -> click.Group:
|
|
55
|
+
# The root group; its callback publishes the invocation context to every command.
|
|
56
|
+
application_context = self._context
|
|
57
|
+
|
|
58
|
+
@click.group(
|
|
59
|
+
name="vidbyte-cli",
|
|
60
|
+
help="Vidbyte CLI: hosted research and local agent runtimes",
|
|
61
|
+
)
|
|
62
|
+
@click.version_option(current_version(), "--version")
|
|
63
|
+
@click.option(
|
|
64
|
+
"--format",
|
|
65
|
+
"output_format",
|
|
66
|
+
type=click.Choice([item.value for item in OutputFormat], case_sensitive=False),
|
|
67
|
+
help="Select human, one-document, streaming, or suppressed result output.",
|
|
68
|
+
)
|
|
69
|
+
@click.option("--json", "as_json", is_flag=True, help="Alias for --format json.")
|
|
70
|
+
@click.option("--profile", type=str, help="Use a named configuration profile.")
|
|
71
|
+
@click.option("--no-input", is_flag=True, help="Never prompt for interactive input.")
|
|
72
|
+
@click.option(
|
|
73
|
+
"--color",
|
|
74
|
+
type=click.Choice([item.value for item in ColorMode], case_sensitive=False),
|
|
75
|
+
# No Click default: an unset flag must stay distinguishable from an explicit
|
|
76
|
+
# `--color auto`, or the option would outrank the stored profile every time.
|
|
77
|
+
default=None,
|
|
78
|
+
)
|
|
79
|
+
@click.option("--debug", is_flag=True, help="Show redacted internal stack frames.")
|
|
80
|
+
@click.pass_context
|
|
81
|
+
def program(click_context: click.Context, /, **values: object) -> None:
|
|
82
|
+
# Rebind root policy from Click, the authoritative parser, then publish context.
|
|
83
|
+
self._configure_context(RootOptionValues.from_click(values))
|
|
84
|
+
click_context.obj = application_context
|
|
85
|
+
|
|
86
|
+
return program
|
|
87
|
+
|
|
88
|
+
def _preconfigure(self, argv: Sequence[str]) -> None:
|
|
89
|
+
# Invalid root syntax stays service-free and is later rendered by Click.
|
|
90
|
+
inspection = RootOptionInspector(argv).inspect()
|
|
91
|
+
if inspection is None:
|
|
92
|
+
return
|
|
93
|
+
# Help and version exit before any command, and resolving configuration would read
|
|
94
|
+
# the filesystem on their behalf.
|
|
95
|
+
if not inspection.exits_before_command:
|
|
96
|
+
self._configure_context(inspection.values)
|
|
97
|
+
|
|
98
|
+
def _invoke(self, program: click.Group, argv: Sequence[str]) -> int:
|
|
99
|
+
# Click writes to the process streams, so bind them to the invocation-owned channels.
|
|
100
|
+
# standalone_mode=False hands click's exceptions back to the trap in run().
|
|
101
|
+
with redirect_stdout(self._streams.stdout), redirect_stderr(self._streams.stderr):
|
|
102
|
+
program.main(args=list(argv[1:]), prog_name="vidbyte-cli", standalone_mode=False)
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
def _configure_context(self, values: RootOptionValues) -> None:
|
|
106
|
+
# Root options are the highest-precedence layer, not the whole answer: the resolver
|
|
107
|
+
# fills every unset field from the environment, the profile, then built-ins.
|
|
108
|
+
resolved = self._context.config_resolver().resolve(
|
|
109
|
+
ConfigOverrides(
|
|
110
|
+
profile=values.profile,
|
|
111
|
+
output_format=self._resolve_output_format(values.output_format, values.as_json),
|
|
112
|
+
color=ColorMode(values.color) if values.color is not None else None,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
self._context.configure(
|
|
116
|
+
InvocationOptions(
|
|
117
|
+
output_format=resolved.output_format,
|
|
118
|
+
profile=resolved.profile,
|
|
119
|
+
api_url=resolved.api_url,
|
|
120
|
+
request_timeout_seconds=resolved.request_timeout_seconds,
|
|
121
|
+
no_input=values.no_input,
|
|
122
|
+
color=resolved.color,
|
|
123
|
+
debug=values.debug,
|
|
124
|
+
),
|
|
125
|
+
resolved,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _resolve_output_format(self, value: str | None, as_json: bool) -> OutputFormat | None:
|
|
129
|
+
# --json is an alias, not an independent mode; duplicate JSON intent is harmless.
|
|
130
|
+
if as_json and value not in {None, OutputFormat.JSON.value}:
|
|
131
|
+
raise ConflictingOutputFormat()
|
|
132
|
+
if as_json:
|
|
133
|
+
return OutputFormat.JSON
|
|
134
|
+
return OutputFormat(value) if value is not None else None
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""The dependency graph and presentation policy owned by exactly one invocation.
|
|
2
|
+
|
|
3
|
+
Commands reach services through here instead of module globals. Everything expensive is
|
|
4
|
+
built lazily, so `--help` and `--version` never touch the keyring, the config file, or the
|
|
5
|
+
network — a keyring lookup can pop an OS unlock dialog, which no help invocation may do.
|
|
6
|
+
|
|
7
|
+
Root options arrive through `configure()` before any command service exists, because output
|
|
8
|
+
policy has to be settled before the first byte is written.
|
|
9
|
+
|
|
10
|
+
The environment is injected rather than read at each use, so a test or an embedding process
|
|
11
|
+
gets one place to control credential and configuration discovery.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from collections.abc import Callable, Mapping
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from ...types.provider import Provider
|
|
21
|
+
from ..api.client import ApiClient
|
|
22
|
+
from ..api.endpoints.billing import BillingEndpoints
|
|
23
|
+
from ..api.endpoints.research import ResearchEndpoints
|
|
24
|
+
from ..api.endpoints.runtime import RuntimeEndpoints
|
|
25
|
+
from ..auth import (
|
|
26
|
+
ApiCredentialVerifier,
|
|
27
|
+
CredentialResolver,
|
|
28
|
+
CredentialStore,
|
|
29
|
+
CredentialVerifier,
|
|
30
|
+
)
|
|
31
|
+
from ..auth.credentials import Credentials
|
|
32
|
+
from ..auth.provider_credentials import ProviderCredentials
|
|
33
|
+
from ..auth.provider_resolver import ProviderResolver
|
|
34
|
+
from ..auth.provider_store import ProviderCredentialStore
|
|
35
|
+
from ..auth.provider_verifier import ProviderVerifier, verifier_for_provider
|
|
36
|
+
from ..config import ConfigResolver, ConfigStore, ResolvedConfig, VidbytePaths
|
|
37
|
+
from ..config.migration import StateMigration
|
|
38
|
+
from ..config.models import DEFAULT_API_URL, DEFAULT_PROFILE
|
|
39
|
+
from ..errors.failures import AuthenticationRequired, ProviderAuthenticationRequired
|
|
40
|
+
from ..errors.handler import ErrorHandler
|
|
41
|
+
from ..io import IOStreams
|
|
42
|
+
from ..io.terminal import TerminalCapabilities, TerminalPolicy
|
|
43
|
+
from ..output.formats import ColorMode, OutputFormat
|
|
44
|
+
from ..output.manager import OutputManager, OutputPolicy
|
|
45
|
+
from ..runtime_primitives import RuntimeExecutor, RuntimeHostRegistry, RuntimeLaunchPlanner
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class InvocationOptions:
|
|
50
|
+
"""Root presentation, profile, and API policy resolved before command execution."""
|
|
51
|
+
|
|
52
|
+
output_format: OutputFormat = OutputFormat.HUMAN
|
|
53
|
+
profile: str = DEFAULT_PROFILE
|
|
54
|
+
api_url: str = DEFAULT_API_URL
|
|
55
|
+
request_timeout_seconds: float = 30.0
|
|
56
|
+
no_input: bool = False
|
|
57
|
+
color: ColorMode = ColorMode.AUTO
|
|
58
|
+
debug: bool = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ApplicationContext:
|
|
62
|
+
"""Invocation-scoped services, shared with commands via click's context object."""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
streams: IOStreams,
|
|
67
|
+
*,
|
|
68
|
+
environment: Mapping[str, str] | None = None,
|
|
69
|
+
paths: VidbytePaths | None = None,
|
|
70
|
+
verifier_factory: Callable[[], CredentialVerifier] | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
# Construction stays side-effect free; factories run on first request only.
|
|
73
|
+
self.streams = streams
|
|
74
|
+
self.environment = dict(os.environ if environment is None else environment)
|
|
75
|
+
self.options = InvocationOptions()
|
|
76
|
+
self._resolved_config: ResolvedConfig | None = None
|
|
77
|
+
self._paths = paths
|
|
78
|
+
self._config_store: ConfigStore | None = None
|
|
79
|
+
self._config_resolver: ConfigResolver | None = None
|
|
80
|
+
self._credential_store: CredentialStore | None = None
|
|
81
|
+
self._credential_resolver: CredentialResolver | None = None
|
|
82
|
+
self._migration: StateMigration | None = None
|
|
83
|
+
self._verifier_factory = verifier_factory or ApiCredentialVerifier
|
|
84
|
+
self._verifier: CredentialVerifier | None = None
|
|
85
|
+
self._output = self._build_output()
|
|
86
|
+
self._errors = ErrorHandler(self._output)
|
|
87
|
+
self._api_client: ApiClient | None = None
|
|
88
|
+
self._research_endpoints: ResearchEndpoints | None = None
|
|
89
|
+
self._runtime_endpoints: RuntimeEndpoints | None = None
|
|
90
|
+
self._billing_endpoints: BillingEndpoints | None = None
|
|
91
|
+
self._runtime_hosts: RuntimeHostRegistry | None = None
|
|
92
|
+
self._runtime_launch_planner: RuntimeLaunchPlanner | None = None
|
|
93
|
+
self._runtime_executor: RuntimeExecutor | None = None
|
|
94
|
+
self._provider_store: ProviderCredentialStore | None = None
|
|
95
|
+
self._provider_resolver: ProviderResolver | None = None
|
|
96
|
+
|
|
97
|
+
def configure(self, options: InvocationOptions, config: ResolvedConfig) -> None:
|
|
98
|
+
# Root options are read twice — once by the pre-scan, once by Click — so an unchanged
|
|
99
|
+
# policy must not rebuild the OutputManager and leave two disagreeing about a stream.
|
|
100
|
+
if options == self.options and config == self._resolved_config:
|
|
101
|
+
return
|
|
102
|
+
self.options = options
|
|
103
|
+
self._resolved_config = config
|
|
104
|
+
self._output = self._build_output()
|
|
105
|
+
self._errors = ErrorHandler(self._output, debug=options.debug)
|
|
106
|
+
|
|
107
|
+
def resolved_config(self) -> ResolvedConfig:
|
|
108
|
+
# A command reached for configuration without going through the root callback,
|
|
109
|
+
# which happens whenever Click short-circuits; resolve it on demand.
|
|
110
|
+
if self._resolved_config is None:
|
|
111
|
+
self._resolved_config = self.config_resolver().resolve()
|
|
112
|
+
return self._resolved_config
|
|
113
|
+
|
|
114
|
+
def paths(self) -> VidbytePaths:
|
|
115
|
+
if self._paths is None:
|
|
116
|
+
self._paths = VidbytePaths.default()
|
|
117
|
+
return self._paths
|
|
118
|
+
|
|
119
|
+
def config_store(self) -> ConfigStore:
|
|
120
|
+
if self._config_store is None:
|
|
121
|
+
self._config_store = ConfigStore(self.paths())
|
|
122
|
+
return self._config_store
|
|
123
|
+
|
|
124
|
+
def config_resolver(self) -> ConfigResolver:
|
|
125
|
+
if self._config_resolver is None:
|
|
126
|
+
self._config_resolver = ConfigResolver(self.config_store(), self.environment)
|
|
127
|
+
return self._config_resolver
|
|
128
|
+
|
|
129
|
+
def credential_store(self) -> CredentialStore:
|
|
130
|
+
if self._credential_store is None:
|
|
131
|
+
self._credential_store = CredentialStore(paths=self.paths())
|
|
132
|
+
return self._credential_store
|
|
133
|
+
|
|
134
|
+
def credential_resolver(self) -> CredentialResolver:
|
|
135
|
+
if self._credential_resolver is None:
|
|
136
|
+
self._credential_resolver = CredentialResolver(
|
|
137
|
+
self.credential_store(),
|
|
138
|
+
self.environment,
|
|
139
|
+
)
|
|
140
|
+
return self._credential_resolver
|
|
141
|
+
|
|
142
|
+
def migration(self) -> StateMigration:
|
|
143
|
+
if self._migration is None:
|
|
144
|
+
self._migration = StateMigration(
|
|
145
|
+
self.paths(),
|
|
146
|
+
self.config_store(),
|
|
147
|
+
self.credential_store().keyring,
|
|
148
|
+
)
|
|
149
|
+
return self._migration
|
|
150
|
+
|
|
151
|
+
def credential_verifier(self) -> CredentialVerifier:
|
|
152
|
+
if self._verifier is None:
|
|
153
|
+
self._verifier = self._verifier_factory()
|
|
154
|
+
return self._verifier
|
|
155
|
+
|
|
156
|
+
def require_credentials(self) -> Credentials:
|
|
157
|
+
# The API key for this invocation's profile and host, or a clean failure if none.
|
|
158
|
+
config = self.resolved_config()
|
|
159
|
+
resolved = self.credential_resolver().resolve(config.profile, config.api_url)
|
|
160
|
+
if resolved is None:
|
|
161
|
+
raise AuthenticationRequired()
|
|
162
|
+
return resolved.credentials
|
|
163
|
+
|
|
164
|
+
def api_client(self) -> ApiClient:
|
|
165
|
+
# One client per invocation, so a polling command reuses a single connection pool.
|
|
166
|
+
if self._api_client is None:
|
|
167
|
+
self._api_client = ApiClient(self.resolved_config(), self.require_credentials())
|
|
168
|
+
return self._api_client
|
|
169
|
+
|
|
170
|
+
def research_endpoints(self) -> ResearchEndpoints:
|
|
171
|
+
# The authenticated research route group; requires a logged-in user.
|
|
172
|
+
if self._research_endpoints is None:
|
|
173
|
+
self._research_endpoints = ResearchEndpoints(self.api_client())
|
|
174
|
+
return self._research_endpoints
|
|
175
|
+
|
|
176
|
+
def runtime_endpoints(self) -> RuntimeEndpoints:
|
|
177
|
+
# Lazily binds runtime HTTP operations to this invocation's authenticated client.
|
|
178
|
+
if self._runtime_endpoints is None:
|
|
179
|
+
self._runtime_endpoints = RuntimeEndpoints(self.api_client())
|
|
180
|
+
return self._runtime_endpoints
|
|
181
|
+
|
|
182
|
+
def billing_endpoints(self) -> BillingEndpoints:
|
|
183
|
+
# Lazily binds billing HTTP operations to this invocation's API client.
|
|
184
|
+
if self._billing_endpoints is None:
|
|
185
|
+
self._billing_endpoints = BillingEndpoints(self.api_client())
|
|
186
|
+
return self._billing_endpoints
|
|
187
|
+
|
|
188
|
+
def runtime_hosts(self) -> RuntimeHostRegistry:
|
|
189
|
+
# Shares one PATH discovery policy between doctor and launch planning.
|
|
190
|
+
if self._runtime_hosts is None:
|
|
191
|
+
self._runtime_hosts = RuntimeHostRegistry()
|
|
192
|
+
return self._runtime_hosts
|
|
193
|
+
|
|
194
|
+
def runtime_launch_planner(self) -> RuntimeLaunchPlanner:
|
|
195
|
+
# Builds local launch plans without credentials, network calls, or subprocesses.
|
|
196
|
+
if self._runtime_launch_planner is None:
|
|
197
|
+
self._runtime_launch_planner = RuntimeLaunchPlanner(self.runtime_hosts())
|
|
198
|
+
return self._runtime_launch_planner
|
|
199
|
+
|
|
200
|
+
def runtime_executor(self) -> RuntimeExecutor:
|
|
201
|
+
# Returns the inert boundary a later runtime implementation will replace.
|
|
202
|
+
if self._runtime_executor is None:
|
|
203
|
+
self._runtime_executor = RuntimeExecutor()
|
|
204
|
+
return self._runtime_executor
|
|
205
|
+
|
|
206
|
+
def close(self) -> None:
|
|
207
|
+
# Releases network resources this invocation opened; a help path opened none.
|
|
208
|
+
if self._api_client is not None:
|
|
209
|
+
self._api_client.close()
|
|
210
|
+
|
|
211
|
+
def output(self) -> OutputManager:
|
|
212
|
+
# Callers share one policy object so stdout cardinality stays enforceable.
|
|
213
|
+
return self._output
|
|
214
|
+
|
|
215
|
+
def provider_store(self) -> ProviderCredentialStore:
|
|
216
|
+
# Lazily created so --help never opens the keyring.
|
|
217
|
+
if self._provider_store is None:
|
|
218
|
+
self._provider_store = ProviderCredentialStore(paths=self.paths())
|
|
219
|
+
return self._provider_store
|
|
220
|
+
|
|
221
|
+
def provider_resolver(self) -> ProviderResolver:
|
|
222
|
+
# Env > keyring > file, scoped by profile+provider.
|
|
223
|
+
if self._provider_resolver is None:
|
|
224
|
+
self._provider_resolver = ProviderResolver(self.provider_store(), self.environment)
|
|
225
|
+
return self._provider_resolver
|
|
226
|
+
|
|
227
|
+
def provider_verifier(self, provider: Provider) -> ProviderVerifier:
|
|
228
|
+
# Factory keeps command from branching on provider.
|
|
229
|
+
return verifier_for_provider(provider)
|
|
230
|
+
|
|
231
|
+
def require_provider_credentials(self, provider: Provider) -> ProviderCredentials:
|
|
232
|
+
# Provider key for this profile, or a typed failure before any network call.
|
|
233
|
+
config = self.resolved_config()
|
|
234
|
+
resolved = self.provider_resolver().resolve(config.profile, provider)
|
|
235
|
+
if resolved is None:
|
|
236
|
+
raise ProviderAuthenticationRequired(provider.value)
|
|
237
|
+
return resolved.credentials
|
|
238
|
+
|
|
239
|
+
def error_handler(self) -> ErrorHandler:
|
|
240
|
+
return self._errors
|
|
241
|
+
|
|
242
|
+
def _build_output(self) -> OutputManager:
|
|
243
|
+
# Terminal detection reruns whenever color or no-input preferences change.
|
|
244
|
+
terminal_policy = TerminalPolicy(
|
|
245
|
+
self.options.color,
|
|
246
|
+
self.options.no_input,
|
|
247
|
+
self.environment,
|
|
248
|
+
)
|
|
249
|
+
terminal = TerminalCapabilities.detect(self.streams, terminal_policy)
|
|
250
|
+
return OutputManager(self.streams, OutputPolicy(self.options.output_format, terminal))
|