devsper 2.1.6__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.
- devsper/__init__.py +14 -0
- devsper/agents/a2a/__init__.py +27 -0
- devsper/agents/a2a/client.py +126 -0
- devsper/agents/a2a/discovery.py +24 -0
- devsper/agents/a2a/server.py +128 -0
- devsper/agents/a2a/tool_adapter.py +68 -0
- devsper/agents/a2a/types.py +49 -0
- devsper/agents/agent.py +602 -0
- devsper/agents/critic.py +80 -0
- devsper/agents/message_bus.py +124 -0
- devsper/agents/roles.py +181 -0
- devsper/agents/run_agent.py +78 -0
- devsper/analytics/__init__.py +5 -0
- devsper/analytics/tool_analytics.py +78 -0
- devsper/audit/__init__.py +5 -0
- devsper/audit/logger.py +214 -0
- devsper/bus/__init__.py +29 -0
- devsper/bus/backends/__init__.py +5 -0
- devsper/bus/backends/base.py +38 -0
- devsper/bus/backends/memory.py +55 -0
- devsper/bus/backends/redis.py +146 -0
- devsper/bus/message.py +56 -0
- devsper/bus/schema_version.py +3 -0
- devsper/bus/topics.py +19 -0
- devsper/cache/__init__.py +6 -0
- devsper/cache/embedding_index.py +98 -0
- devsper/cache/hashing.py +24 -0
- devsper/cache/store.py +153 -0
- devsper/cache/task_cache.py +191 -0
- devsper/cli/__init__.py +6 -0
- devsper/cli/commands/reg.py +733 -0
- devsper/cli/github_oauth.py +157 -0
- devsper/cli/init.py +637 -0
- devsper/cli/main.py +2956 -0
- devsper/cli/run_progress.py +103 -0
- devsper/cli/ui/__init__.py +65 -0
- devsper/cli/ui/components.py +94 -0
- devsper/cli/ui/errors.py +104 -0
- devsper/cli/ui/logging.py +120 -0
- devsper/cli/ui/onboarding.py +102 -0
- devsper/cli/ui/progress.py +43 -0
- devsper/cli/ui/run_view.py +308 -0
- devsper/cli/ui/theme.py +40 -0
- devsper/cluster/__init__.py +29 -0
- devsper/cluster/election.py +84 -0
- devsper/cluster/local.py +97 -0
- devsper/cluster/node_info.py +77 -0
- devsper/cluster/registry.py +71 -0
- devsper/cluster/router.py +117 -0
- devsper/cluster/state_backend.py +105 -0
- devsper/compliance/__init__.py +5 -0
- devsper/compliance/pii.py +147 -0
- devsper/config/__init__.py +52 -0
- devsper/config/config_loader.py +121 -0
- devsper/config/defaults.py +77 -0
- devsper/config/resolver.py +342 -0
- devsper/config/schema.py +237 -0
- devsper/credentials/__init__.py +19 -0
- devsper/credentials/cli.py +197 -0
- devsper/credentials/migration.py +124 -0
- devsper/credentials/store.py +142 -0
- devsper/dashboard/__init__.py +9 -0
- devsper/dashboard/dashboard.py +87 -0
- devsper/dev/__init__.py +25 -0
- devsper/dev/builder.py +195 -0
- devsper/dev/debugger.py +95 -0
- devsper/dev/repo_index.py +138 -0
- devsper/dev/sandbox.py +203 -0
- devsper/dev/scaffold.py +122 -0
- devsper/embeddings/__init__.py +5 -0
- devsper/embeddings/service.py +36 -0
- devsper/explainability/__init__.py +14 -0
- devsper/explainability/decision_tree.py +104 -0
- devsper/explainability/rationale.py +38 -0
- devsper/explainability/simulation.py +56 -0
- devsper/hitl/__init__.py +13 -0
- devsper/hitl/approval.py +160 -0
- devsper/hitl/escalation.py +95 -0
- devsper/intelligence/__init__.py +9 -0
- devsper/intelligence/adaptation.py +88 -0
- devsper/intelligence/analysis/__init__.py +19 -0
- devsper/intelligence/analysis/analyzer.py +71 -0
- devsper/intelligence/analysis/cost_estimator.py +66 -0
- devsper/intelligence/analysis/formatter.py +103 -0
- devsper/intelligence/analysis/run_report.py +402 -0
- devsper/intelligence/learning_engine.py +92 -0
- devsper/intelligence/strategies/__init__.py +23 -0
- devsper/intelligence/strategies/base.py +14 -0
- devsper/intelligence/strategies/code_analysis_strategy.py +33 -0
- devsper/intelligence/strategies/data_science_strategy.py +33 -0
- devsper/intelligence/strategies/document_pipeline_strategy.py +33 -0
- devsper/intelligence/strategies/experiment_strategy.py +33 -0
- devsper/intelligence/strategies/research_strategy.py +34 -0
- devsper/intelligence/strategy_selector.py +84 -0
- devsper/intelligence/synthesis.py +132 -0
- devsper/intelligence/task_optimizer.py +92 -0
- devsper/knowledge/__init__.py +5 -0
- devsper/knowledge/extractor.py +204 -0
- devsper/knowledge/knowledge_graph.py +184 -0
- devsper/knowledge/query.py +285 -0
- devsper/memory/__init__.py +35 -0
- devsper/memory/consolidation.py +138 -0
- devsper/memory/embeddings.py +60 -0
- devsper/memory/memory_index.py +97 -0
- devsper/memory/memory_router.py +62 -0
- devsper/memory/memory_store.py +221 -0
- devsper/memory/memory_types.py +54 -0
- devsper/memory/namespaces.py +45 -0
- devsper/memory/scoring.py +77 -0
- devsper/memory/summarizer.py +52 -0
- devsper/nodes/__init__.py +5 -0
- devsper/nodes/controller.py +449 -0
- devsper/nodes/rpc.py +127 -0
- devsper/nodes/single.py +161 -0
- devsper/nodes/worker.py +506 -0
- devsper/orchestration/__init__.py +19 -0
- devsper/orchestration/meta_planner.py +239 -0
- devsper/orchestration/priority_queue.py +61 -0
- devsper/plugins/__init__.py +19 -0
- devsper/plugins/marketplace/__init__.py +0 -0
- devsper/plugins/plugin_loader.py +70 -0
- devsper/plugins/plugin_registry.py +34 -0
- devsper/plugins/registry.py +83 -0
- devsper/protocols/__init__.py +6 -0
- devsper/providers/__init__.py +17 -0
- devsper/providers/anthropic.py +84 -0
- devsper/providers/base.py +75 -0
- devsper/providers/complexity_router.py +94 -0
- devsper/providers/gemini.py +36 -0
- devsper/providers/github.py +180 -0
- devsper/providers/model_router.py +40 -0
- devsper/providers/openai.py +105 -0
- devsper/providers/router/__init__.py +21 -0
- devsper/providers/router/backends/__init__.py +19 -0
- devsper/providers/router/backends/anthropic_backend.py +111 -0
- devsper/providers/router/backends/custom_backend.py +138 -0
- devsper/providers/router/backends/gemini_backend.py +89 -0
- devsper/providers/router/backends/github_backend.py +165 -0
- devsper/providers/router/backends/ollama_backend.py +104 -0
- devsper/providers/router/backends/openai_backend.py +142 -0
- devsper/providers/router/backends/vllm_backend.py +35 -0
- devsper/providers/router/base.py +60 -0
- devsper/providers/router/factory.py +92 -0
- devsper/providers/router/legacy.py +101 -0
- devsper/providers/router/router.py +135 -0
- devsper/reasoning/__init__.py +12 -0
- devsper/reasoning/graph.py +59 -0
- devsper/reasoning/nodes.py +20 -0
- devsper/reasoning/store.py +67 -0
- devsper/runtime/__init__.py +12 -0
- devsper/runtime/health.py +88 -0
- devsper/runtime/replay.py +53 -0
- devsper/runtime/replay_engine.py +142 -0
- devsper/runtime/run_history.py +204 -0
- devsper/runtime/telemetry.py +116 -0
- devsper/runtime/visualize.py +58 -0
- devsper/sandbox/__init__.py +13 -0
- devsper/sandbox/sandbox.py +161 -0
- devsper/swarm/checkpointer.py +65 -0
- devsper/swarm/executor.py +558 -0
- devsper/swarm/map_reduce.py +44 -0
- devsper/swarm/planner.py +197 -0
- devsper/swarm/prefetcher.py +91 -0
- devsper/swarm/scheduler.py +153 -0
- devsper/swarm/speculation.py +47 -0
- devsper/swarm/swarm.py +562 -0
- devsper/tools/__init__.py +33 -0
- devsper/tools/base.py +29 -0
- devsper/tools/code_intelligence/__init__.py +13 -0
- devsper/tools/code_intelligence/api_surface_extractor.py +73 -0
- devsper/tools/code_intelligence/architecture_analyzer.py +65 -0
- devsper/tools/code_intelligence/codebase_indexer.py +71 -0
- devsper/tools/code_intelligence/dependency_graph_builder.py +67 -0
- devsper/tools/code_intelligence/design_pattern_detector.py +62 -0
- devsper/tools/code_intelligence/large_function_detector.py +68 -0
- devsper/tools/code_intelligence/module_responsibility_mapper.py +56 -0
- devsper/tools/code_intelligence/parallel_codebase_analysis.py +44 -0
- devsper/tools/code_intelligence/refactor_candidate_detector.py +81 -0
- devsper/tools/code_intelligence/repository_semantic_index.py +61 -0
- devsper/tools/code_intelligence/test_coverage_estimator.py +62 -0
- devsper/tools/coding/__init__.py +12 -0
- devsper/tools/coding/analyze_code_complexity.py +48 -0
- devsper/tools/coding/dependency_analyzer.py +42 -0
- devsper/tools/coding/extract_functions.py +38 -0
- devsper/tools/coding/format_python.py +50 -0
- devsper/tools/coding/generate_docstrings.py +40 -0
- devsper/tools/coding/generate_unit_tests.py +42 -0
- devsper/tools/coding/lint_python.py +51 -0
- devsper/tools/coding/refactor_function.py +41 -0
- devsper/tools/coding/repo_structure_map.py +54 -0
- devsper/tools/coding/run_python.py +53 -0
- devsper/tools/data/__init__.py +12 -0
- devsper/tools/data/column_type_detection.py +64 -0
- devsper/tools/data/csv_summary.py +52 -0
- devsper/tools/data/dataframe_filter.py +51 -0
- devsper/tools/data/dataframe_groupby.py +47 -0
- devsper/tools/data/dataframe_stats.py +38 -0
- devsper/tools/data/dataset_sampling.py +55 -0
- devsper/tools/data/dataset_schema.py +45 -0
- devsper/tools/data/json_pretty_print.py +37 -0
- devsper/tools/data/json_query.py +46 -0
- devsper/tools/data/missing_value_report.py +47 -0
- devsper/tools/data_science/__init__.py +13 -0
- devsper/tools/data_science/correlation_heatmap.py +72 -0
- devsper/tools/data_science/dataset_bias_detector.py +49 -0
- devsper/tools/data_science/dataset_distribution_report.py +64 -0
- devsper/tools/data_science/dataset_drift_detector.py +64 -0
- devsper/tools/data_science/dataset_outlier_detector.py +65 -0
- devsper/tools/data_science/dataset_profile.py +76 -0
- devsper/tools/data_science/distributed_dataset_processor.py +54 -0
- devsper/tools/data_science/feature_engineering_suggestions.py +69 -0
- devsper/tools/data_science/feature_importance_estimator.py +82 -0
- devsper/tools/data_science/model_input_validator.py +59 -0
- devsper/tools/data_science/time_series_analyzer.py +57 -0
- devsper/tools/documents/__init__.py +11 -0
- devsper/tools/documents/_docproc.py +56 -0
- devsper/tools/documents/document_to_markdown.py +29 -0
- devsper/tools/documents/extract_document_images.py +39 -0
- devsper/tools/documents/extract_document_text.py +29 -0
- devsper/tools/documents/extract_equations.py +36 -0
- devsper/tools/documents/extract_tables.py +47 -0
- devsper/tools/documents/summarize_document.py +42 -0
- devsper/tools/documents/write_latex_document.py +133 -0
- devsper/tools/documents/write_markdown_document.py +89 -0
- devsper/tools/documents/write_word_document.py +149 -0
- devsper/tools/experiments/__init__.py +13 -0
- devsper/tools/experiments/bootstrap_estimator.py +54 -0
- devsper/tools/experiments/experiment_report_generator.py +50 -0
- devsper/tools/experiments/experiment_tracker.py +36 -0
- devsper/tools/experiments/grid_search_runner.py +50 -0
- devsper/tools/experiments/model_benchmark_runner.py +45 -0
- devsper/tools/experiments/monte_carlo_experiment.py +38 -0
- devsper/tools/experiments/parameter_sweep_runner.py +51 -0
- devsper/tools/experiments/result_comparator.py +58 -0
- devsper/tools/experiments/simulation_runner.py +43 -0
- devsper/tools/experiments/statistical_significance_test.py +56 -0
- devsper/tools/experiments/swarm_map_reduce.py +42 -0
- devsper/tools/filesystem/__init__.py +12 -0
- devsper/tools/filesystem/append_file.py +42 -0
- devsper/tools/filesystem/file_hash.py +40 -0
- devsper/tools/filesystem/file_line_count.py +36 -0
- devsper/tools/filesystem/file_metadata.py +38 -0
- devsper/tools/filesystem/file_preview.py +55 -0
- devsper/tools/filesystem/find_large_files.py +50 -0
- devsper/tools/filesystem/list_directory.py +39 -0
- devsper/tools/filesystem/read_file.py +35 -0
- devsper/tools/filesystem/search_files.py +60 -0
- devsper/tools/filesystem/write_file.py +41 -0
- devsper/tools/flagship/__init__.py +15 -0
- devsper/tools/flagship/distributed_document_analysis.py +77 -0
- devsper/tools/flagship/docproc_corpus_pipeline.py +91 -0
- devsper/tools/flagship/repository_semantic_map.py +99 -0
- devsper/tools/flagship/research_graph_builder.py +111 -0
- devsper/tools/flagship/swarm_experiment_runner.py +86 -0
- devsper/tools/knowledge/__init__.py +10 -0
- devsper/tools/knowledge/citation_graph_builder.py +69 -0
- devsper/tools/knowledge/concept_frequency_analyzer.py +74 -0
- devsper/tools/knowledge/corpus_builder.py +66 -0
- devsper/tools/knowledge/cross_document_entity_linker.py +71 -0
- devsper/tools/knowledge/document_corpus_summary.py +68 -0
- devsper/tools/knowledge/document_topic_extractor.py +58 -0
- devsper/tools/knowledge/knowledge_graph_extractor.py +58 -0
- devsper/tools/knowledge/timeline_extractor.py +59 -0
- devsper/tools/math/__init__.py +12 -0
- devsper/tools/math/calculate_expression.py +52 -0
- devsper/tools/math/correlation.py +44 -0
- devsper/tools/math/distribution_summary.py +39 -0
- devsper/tools/math/histogram.py +53 -0
- devsper/tools/math/linear_regression.py +47 -0
- devsper/tools/math/matrix_multiply.py +38 -0
- devsper/tools/math/mean_std.py +35 -0
- devsper/tools/math/monte_carlo_simulation.py +43 -0
- devsper/tools/math/polynomial_fit.py +40 -0
- devsper/tools/math/random_sample.py +36 -0
- devsper/tools/mcp/__init__.py +23 -0
- devsper/tools/mcp/adapter.py +53 -0
- devsper/tools/mcp/client.py +235 -0
- devsper/tools/mcp/discovery.py +53 -0
- devsper/tools/memory/__init__.py +16 -0
- devsper/tools/memory/delete_memory.py +25 -0
- devsper/tools/memory/list_memory.py +34 -0
- devsper/tools/memory/search_memory.py +36 -0
- devsper/tools/memory/store_memory.py +47 -0
- devsper/tools/memory/summarize_memory.py +41 -0
- devsper/tools/memory/tag_memory.py +47 -0
- devsper/tools/pipelines.py +92 -0
- devsper/tools/registry.py +39 -0
- devsper/tools/research/__init__.py +12 -0
- devsper/tools/research/arxiv_download.py +55 -0
- devsper/tools/research/arxiv_search.py +58 -0
- devsper/tools/research/citation_extractor.py +35 -0
- devsper/tools/research/duckduckgo_search.py +42 -0
- devsper/tools/research/paper_metadata_extractor.py +45 -0
- devsper/tools/research/paper_summarizer.py +41 -0
- devsper/tools/research/research_question_generator.py +39 -0
- devsper/tools/research/topic_cluster.py +46 -0
- devsper/tools/research/web_search.py +47 -0
- devsper/tools/research/wikipedia_lookup.py +50 -0
- devsper/tools/research_advanced/__init__.py +14 -0
- devsper/tools/research_advanced/citation_context_extractor.py +60 -0
- devsper/tools/research_advanced/literature_review_generator.py +79 -0
- devsper/tools/research_advanced/methodology_extractor.py +58 -0
- devsper/tools/research_advanced/paper_contribution_extractor.py +50 -0
- devsper/tools/research_advanced/paper_dataset_identifier.py +49 -0
- devsper/tools/research_advanced/paper_method_comparator.py +62 -0
- devsper/tools/research_advanced/paper_similarity_search.py +69 -0
- devsper/tools/research_advanced/paper_trend_analyzer.py +69 -0
- devsper/tools/research_advanced/parallel_document_analyzer.py +56 -0
- devsper/tools/research_advanced/research_gap_finder.py +71 -0
- devsper/tools/research_advanced/research_topic_mapper.py +69 -0
- devsper/tools/research_advanced/swarm_literature_review.py +58 -0
- devsper/tools/scoring/__init__.py +52 -0
- devsper/tools/scoring/report.py +44 -0
- devsper/tools/scoring/scorer.py +39 -0
- devsper/tools/scoring/selector.py +61 -0
- devsper/tools/scoring/store.py +267 -0
- devsper/tools/selector.py +130 -0
- devsper/tools/system/__init__.py +12 -0
- devsper/tools/system/cpu_usage.py +22 -0
- devsper/tools/system/disk_usage.py +35 -0
- devsper/tools/system/environment_variables.py +29 -0
- devsper/tools/system/memory_usage.py +23 -0
- devsper/tools/system/pip_install.py +44 -0
- devsper/tools/system/pip_search.py +29 -0
- devsper/tools/system/process_list.py +34 -0
- devsper/tools/system/python_package_list.py +40 -0
- devsper/tools/system/run_shell_command.py +51 -0
- devsper/tools/system/system_info.py +26 -0
- devsper/tools/tool_runner.py +122 -0
- devsper/tui/__init__.py +5 -0
- devsper/tui/activity_feed_view.py +73 -0
- devsper/tui/adaptive_tasks_view.py +75 -0
- devsper/tui/agent_role_view.py +35 -0
- devsper/tui/app.py +395 -0
- devsper/tui/dashboard_screen.py +290 -0
- devsper/tui/dev_view.py +99 -0
- devsper/tui/inject_screen.py +73 -0
- devsper/tui/knowledge_graph_view.py +46 -0
- devsper/tui/layout.py +43 -0
- devsper/tui/logs_view.py +83 -0
- devsper/tui/memory_view.py +58 -0
- devsper/tui/performance_view.py +33 -0
- devsper/tui/reasoning_graph_view.py +39 -0
- devsper/tui/results_view.py +139 -0
- devsper/tui/swarm_view.py +37 -0
- devsper/tui/task_detail_screen.py +55 -0
- devsper/tui/task_view.py +103 -0
- devsper/types/event.py +97 -0
- devsper/types/exceptions.py +21 -0
- devsper/types/swarm.py +41 -0
- devsper/types/task.py +80 -0
- devsper/upgrade/__init__.py +21 -0
- devsper/upgrade/changelog.py +124 -0
- devsper/upgrade/cli.py +145 -0
- devsper/upgrade/installer.py +103 -0
- devsper/upgrade/notifier.py +52 -0
- devsper/upgrade/version_check.py +121 -0
- devsper/utils/event_logger.py +88 -0
- devsper/utils/http.py +43 -0
- devsper/utils/models.py +54 -0
- devsper/visualization/__init__.py +5 -0
- devsper/visualization/dag_export.py +67 -0
- devsper/workflow/__init__.py +18 -0
- devsper/workflow/conditions.py +157 -0
- devsper/workflow/context.py +108 -0
- devsper/workflow/loader.py +156 -0
- devsper/workflow/resolver.py +109 -0
- devsper/workflow/runner.py +562 -0
- devsper/workflow/schema.py +63 -0
- devsper/workflow/validator.py +128 -0
- devsper-2.1.6.dist-info/METADATA +346 -0
- devsper-2.1.6.dist-info/RECORD +375 -0
- devsper-2.1.6.dist-info/WHEEL +4 -0
- devsper-2.1.6.dist-info/entry_points.txt +3 -0
- devsper-2.1.6.dist-info/licenses/LICENSE +639 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
"""devsper reg — registry CLI commands.
|
|
2
|
+
|
|
3
|
+
Subcommands: login, logout, whoami, test, publish, search, info, versions, yank.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import tomllib
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.live import Live
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
from devsper.plugins.registry import (
|
|
21
|
+
REGISTRY_URL,
|
|
22
|
+
RegistryClient,
|
|
23
|
+
delete_token,
|
|
24
|
+
get_token,
|
|
25
|
+
require_token,
|
|
26
|
+
set_token,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
console = Console()
|
|
30
|
+
|
|
31
|
+
SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── login ──────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cmd_login(args):
|
|
38
|
+
"""Device-flow login: open browser to approve, CLI polls until done."""
|
|
39
|
+
client = RegistryClient()
|
|
40
|
+
|
|
41
|
+
# 1. Request device code
|
|
42
|
+
try:
|
|
43
|
+
r = client.post("/api/v1/auth/device/request", json={})
|
|
44
|
+
r.raise_for_status()
|
|
45
|
+
except Exception as e:
|
|
46
|
+
console.print(f"[red]Could not reach registry:[/red] {e}")
|
|
47
|
+
raise SystemExit(1)
|
|
48
|
+
|
|
49
|
+
data = r.json()
|
|
50
|
+
device_code = data["device_code"]
|
|
51
|
+
user_code = data["user_code"]
|
|
52
|
+
verify_uri = data["verification_uri"]
|
|
53
|
+
expires_in = data.get("expires_in", 300)
|
|
54
|
+
poll_interval = data.get("interval", 5)
|
|
55
|
+
|
|
56
|
+
# 2. Display instructions
|
|
57
|
+
console.print(
|
|
58
|
+
Panel(
|
|
59
|
+
f"[bold]Open:[/bold] {verify_uri}\n"
|
|
60
|
+
f"[bold]Code:[/bold] [yellow]{user_code}[/yellow]\n\n"
|
|
61
|
+
"Waiting for you to approve in the browser...",
|
|
62
|
+
title="[bold]devsper registry login[/bold]",
|
|
63
|
+
border_style="dim",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# 3. Poll for authorization
|
|
68
|
+
deadline = time.time() + expires_in
|
|
69
|
+
frame_i = 0
|
|
70
|
+
|
|
71
|
+
with Live(console=console, refresh_per_second=8) as live:
|
|
72
|
+
while time.time() < deadline:
|
|
73
|
+
remaining = int(deadline - time.time())
|
|
74
|
+
live.update(
|
|
75
|
+
f" {SPINNER_FRAMES[frame_i % len(SPINNER_FRAMES)]} "
|
|
76
|
+
f"[dim]Waiting for authorization ({remaining}s remaining)...[/dim]"
|
|
77
|
+
)
|
|
78
|
+
frame_i += 1
|
|
79
|
+
time.sleep(poll_interval)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
pr = client.post(
|
|
83
|
+
"/api/v1/auth/device/poll",
|
|
84
|
+
json={"device_code": device_code},
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
continue # network blip — keep trying
|
|
88
|
+
|
|
89
|
+
if pr.status_code == 200:
|
|
90
|
+
token = pr.json().get("token")
|
|
91
|
+
if token:
|
|
92
|
+
live.stop()
|
|
93
|
+
set_token(token)
|
|
94
|
+
console.print(
|
|
95
|
+
"\n[green]✓ Logged in.[/green] "
|
|
96
|
+
"Token stored in OS keychain.\n"
|
|
97
|
+
"Run [bold]devsper reg whoami[/bold] to verify."
|
|
98
|
+
)
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
elif pr.status_code == 400:
|
|
102
|
+
live.stop()
|
|
103
|
+
console.print("\n[red]✗ Authorization denied.[/red]")
|
|
104
|
+
raise SystemExit(1)
|
|
105
|
+
|
|
106
|
+
elif pr.status_code == 410:
|
|
107
|
+
live.stop()
|
|
108
|
+
console.print(
|
|
109
|
+
"\n[red]✗ Code expired.[/red] "
|
|
110
|
+
"Run [bold]devsper reg login[/bold] again."
|
|
111
|
+
)
|
|
112
|
+
raise SystemExit(1)
|
|
113
|
+
|
|
114
|
+
# 202 = still pending — keep looping
|
|
115
|
+
|
|
116
|
+
console.print("\n[red]✗ Timed out waiting for authorization.[/red]")
|
|
117
|
+
raise SystemExit(1)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ── logout ─────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def cmd_logout(args):
|
|
124
|
+
delete_token()
|
|
125
|
+
console.print("[green]✓ Logged out.[/green]")
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ── whoami ─────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def cmd_whoami(args):
|
|
133
|
+
token = require_token()
|
|
134
|
+
client = RegistryClient(token)
|
|
135
|
+
try:
|
|
136
|
+
r = client.get("/api/v1/me")
|
|
137
|
+
except Exception as e:
|
|
138
|
+
console.print(f"[red]Error contacting registry:[/red] {e}")
|
|
139
|
+
return 1
|
|
140
|
+
|
|
141
|
+
if r.status_code == 401:
|
|
142
|
+
console.print(
|
|
143
|
+
"[red]Token invalid or expired.[/red] "
|
|
144
|
+
"Run [bold]devsper reg login[/bold] again."
|
|
145
|
+
)
|
|
146
|
+
raise SystemExit(1)
|
|
147
|
+
r.raise_for_status()
|
|
148
|
+
|
|
149
|
+
d = r.json()
|
|
150
|
+
masked = token[:6] + "••••••••••••" + token[-4:]
|
|
151
|
+
username = d.get("username") or d.get("email") or d.get("id") or "—"
|
|
152
|
+
console.print(f"[bold]Username:[/bold] {username}")
|
|
153
|
+
if d.get("email"):
|
|
154
|
+
console.print(f"[bold]Email:[/bold] {d['email']}")
|
|
155
|
+
console.print(f"[bold]ID:[/bold] {d.get('id', '—')}")
|
|
156
|
+
console.print(f"[bold]API key:[/bold] {masked}")
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── test ───────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def cmd_test(args):
|
|
164
|
+
"""Validate that the directory is a publishable devsper plugin."""
|
|
165
|
+
directory = getattr(args, "dir", ".")
|
|
166
|
+
path = Path(directory).resolve()
|
|
167
|
+
passed = 0
|
|
168
|
+
failed = 0
|
|
169
|
+
|
|
170
|
+
def check(desc: str, ok: bool, warn: bool = False):
|
|
171
|
+
nonlocal passed, failed
|
|
172
|
+
if ok:
|
|
173
|
+
console.print(f" [green]✓[/green] {desc}")
|
|
174
|
+
passed += 1
|
|
175
|
+
elif warn:
|
|
176
|
+
console.print(f" [yellow]⚠[/yellow] {desc} [dim](warning)[/dim]")
|
|
177
|
+
else:
|
|
178
|
+
console.print(f" [red]✗[/red] {desc}")
|
|
179
|
+
failed += 1
|
|
180
|
+
|
|
181
|
+
console.print(f"\n[bold]Validating plugin:[/bold] {path}\n")
|
|
182
|
+
|
|
183
|
+
# pyproject.toml
|
|
184
|
+
pyproject_path = path / "pyproject.toml"
|
|
185
|
+
has_pyproject = pyproject_path.exists()
|
|
186
|
+
check("pyproject.toml exists", has_pyproject)
|
|
187
|
+
if not has_pyproject:
|
|
188
|
+
console.print("\n[red]Cannot continue without pyproject.toml[/red]")
|
|
189
|
+
raise SystemExit(1)
|
|
190
|
+
|
|
191
|
+
with open(pyproject_path, "rb") as f:
|
|
192
|
+
pyproject = tomllib.load(f)
|
|
193
|
+
|
|
194
|
+
project = pyproject.get("project", {})
|
|
195
|
+
eps = project.get("entry-points", {})
|
|
196
|
+
plugin_eps = eps.get("devsper.plugins", {})
|
|
197
|
+
|
|
198
|
+
check('[project.entry-points."devsper.plugins"] present', bool(plugin_eps))
|
|
199
|
+
check("version field present", bool(project.get("version")))
|
|
200
|
+
check("description field present", bool(project.get("description")))
|
|
201
|
+
check("license field present", bool(project.get("license")))
|
|
202
|
+
|
|
203
|
+
requires_python = project.get("requires-python", "")
|
|
204
|
+
# Accept any constraint that allows 3.12+ (e.g. ">=3.10", ">=3.12", ">=3.13")
|
|
205
|
+
rp_ok = False
|
|
206
|
+
if requires_python:
|
|
207
|
+
import re
|
|
208
|
+
|
|
209
|
+
# Extract version numbers from the constraint
|
|
210
|
+
versions = re.findall(r"(\d+)\.(\d+)", requires_python)
|
|
211
|
+
for major, minor in versions:
|
|
212
|
+
if int(major) == 3 and int(minor) <= 12:
|
|
213
|
+
rp_ok = True
|
|
214
|
+
break
|
|
215
|
+
if int(major) == 3 and int(minor) > 12:
|
|
216
|
+
# e.g. >=3.13 also fine
|
|
217
|
+
rp_ok = True
|
|
218
|
+
break
|
|
219
|
+
check("requires-python allows 3.12+", rp_ok)
|
|
220
|
+
|
|
221
|
+
# requires-devsper (custom metadata — warn only)
|
|
222
|
+
raw_text = pyproject_path.read_text()
|
|
223
|
+
check("requires-devsper field present", "requires-devsper" in raw_text, warn=True)
|
|
224
|
+
|
|
225
|
+
# Entry point loads
|
|
226
|
+
if plugin_eps:
|
|
227
|
+
ep_value = list(plugin_eps.values())[0]
|
|
228
|
+
module_path, _, func = ep_value.partition(":")
|
|
229
|
+
func = func or "register"
|
|
230
|
+
# Test that the entry point loads and returns a list (or None)
|
|
231
|
+
load_script = (
|
|
232
|
+
f"import sys; sys.path.insert(0,'.'); "
|
|
233
|
+
f"from {module_path} import {func}; result = {func}(); "
|
|
234
|
+
f"r = result if result is not None else []; "
|
|
235
|
+
f"assert isinstance(r, list), 'must return list or None'; "
|
|
236
|
+
f"tool_objs = [t for t in r if hasattr(t,'name') and hasattr(t,'run')]; "
|
|
237
|
+
f"str_names = [t for t in r if isinstance(t, str)]; "
|
|
238
|
+
f"none_flag = '1' if result is None else '0'; "
|
|
239
|
+
f"print(f'{{len(tool_objs)}}:{{len(str_names)}}:{{none_flag}}')"
|
|
240
|
+
)
|
|
241
|
+
result = subprocess.run(
|
|
242
|
+
[sys.executable, "-c", load_script],
|
|
243
|
+
capture_output=True,
|
|
244
|
+
text=True,
|
|
245
|
+
cwd=path,
|
|
246
|
+
)
|
|
247
|
+
loads_ok = result.returncode == 0
|
|
248
|
+
check("Entry point loads without error", loads_ok)
|
|
249
|
+
if loads_ok:
|
|
250
|
+
output = result.stdout.strip()
|
|
251
|
+
parts = output.split(":")
|
|
252
|
+
tool_obj_count = int(parts[0]) if parts[0] else 0
|
|
253
|
+
str_name_count = int(parts[1]) if len(parts) > 1 and parts[1] else 0
|
|
254
|
+
is_none = parts[2] == "1" if len(parts) > 2 else False
|
|
255
|
+
total_items = tool_obj_count + str_name_count
|
|
256
|
+
|
|
257
|
+
if is_none and total_items == 0:
|
|
258
|
+
check("Entry point returns (self-registers, returns None)", True)
|
|
259
|
+
else:
|
|
260
|
+
check(
|
|
261
|
+
f"Entry point returns {total_items} item(s) "
|
|
262
|
+
f"({tool_obj_count} Tool object(s), {str_name_count} name(s))",
|
|
263
|
+
total_items > 0,
|
|
264
|
+
)
|
|
265
|
+
# Validate Tool objects have required attributes
|
|
266
|
+
if tool_obj_count > 0:
|
|
267
|
+
validate = subprocess.run(
|
|
268
|
+
[
|
|
269
|
+
sys.executable,
|
|
270
|
+
"-c",
|
|
271
|
+
f"import sys; sys.path.insert(0,'.'); "
|
|
272
|
+
f"from {module_path} import {func}; result = {func}(); "
|
|
273
|
+
f"tools = [t for t in result if hasattr(t,'name') and hasattr(t,'run')]; "
|
|
274
|
+
f"assert all(hasattr(t,'name') and hasattr(t,'description') "
|
|
275
|
+
f"and hasattr(t,'run') for t in tools), "
|
|
276
|
+
f"'Tool missing required attribute'; "
|
|
277
|
+
f"print('ok')",
|
|
278
|
+
],
|
|
279
|
+
capture_output=True,
|
|
280
|
+
text=True,
|
|
281
|
+
cwd=path,
|
|
282
|
+
)
|
|
283
|
+
check(
|
|
284
|
+
"Each Tool object has name, description, run()",
|
|
285
|
+
validate.returncode == 0,
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
console.print(f" [dim]{result.stderr.strip()}[/dim]")
|
|
289
|
+
else:
|
|
290
|
+
check("Entry point loads without error", False)
|
|
291
|
+
|
|
292
|
+
console.print()
|
|
293
|
+
total = passed + failed
|
|
294
|
+
if failed == 0:
|
|
295
|
+
console.print(
|
|
296
|
+
f"[green]✓ {passed}/{total} checks passed.[/green] "
|
|
297
|
+
"Plugin is ready to publish.\n"
|
|
298
|
+
"Run [bold]devsper reg publish[/bold] to publish."
|
|
299
|
+
)
|
|
300
|
+
else:
|
|
301
|
+
console.print(
|
|
302
|
+
f"[red]✗ {failed} check(s) failed.[/red] {passed}/{total} passed."
|
|
303
|
+
)
|
|
304
|
+
raise SystemExit(1)
|
|
305
|
+
return 0
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ── publish ────────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _read_pyproject(path: Path) -> dict:
|
|
312
|
+
"""Read and return parsed pyproject.toml from the given directory."""
|
|
313
|
+
pyproject_path = path / "pyproject.toml"
|
|
314
|
+
if not pyproject_path.exists():
|
|
315
|
+
return {}
|
|
316
|
+
with open(pyproject_path, "rb") as f:
|
|
317
|
+
return tomllib.load(f)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _ensure_package_exists(client: RegistryClient, name: str, meta: dict) -> None:
|
|
321
|
+
"""Check if package exists on registry; if not, create it from pyproject metadata."""
|
|
322
|
+
r = client.get(f"/api/v1/packages/{name}")
|
|
323
|
+
if r.status_code == 200:
|
|
324
|
+
return # already exists
|
|
325
|
+
|
|
326
|
+
if r.status_code != 404:
|
|
327
|
+
# Unexpected error
|
|
328
|
+
console.print(f"[red]Error checking package ({r.status_code}):[/red] {r.text}")
|
|
329
|
+
raise SystemExit(1)
|
|
330
|
+
|
|
331
|
+
# Package doesn't exist — create it
|
|
332
|
+
project = meta.get("project", {})
|
|
333
|
+
description = project.get("description", "")
|
|
334
|
+
license_val = project.get("license", "")
|
|
335
|
+
# license can be a string or a dict like {text: "MIT"}
|
|
336
|
+
if isinstance(license_val, dict):
|
|
337
|
+
license_val = license_val.get("text", license_val.get("file", ""))
|
|
338
|
+
homepage = ""
|
|
339
|
+
repository = ""
|
|
340
|
+
urls = project.get("urls", {})
|
|
341
|
+
if urls:
|
|
342
|
+
homepage = urls.get("Homepage", urls.get("homepage", ""))
|
|
343
|
+
repository = urls.get(
|
|
344
|
+
"Repository",
|
|
345
|
+
urls.get("repository", urls.get("Source", urls.get("source", ""))),
|
|
346
|
+
)
|
|
347
|
+
keywords = project.get("keywords", [])
|
|
348
|
+
|
|
349
|
+
console.print(
|
|
350
|
+
f"\n[yellow]Package '{name}' does not exist on the registry.[/yellow]\n"
|
|
351
|
+
f" Creating it from pyproject.toml metadata..."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
cr = client.post(
|
|
355
|
+
"/api/v1/packages",
|
|
356
|
+
json={
|
|
357
|
+
"Name": name,
|
|
358
|
+
"DisplayName": project.get("name", name),
|
|
359
|
+
"Description": description,
|
|
360
|
+
"Homepage": homepage or "",
|
|
361
|
+
"Repository": repository or "",
|
|
362
|
+
"License": license_val,
|
|
363
|
+
"Keywords": keywords,
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if cr.status_code == 201:
|
|
368
|
+
console.print(f" [green]✓ Package '{name}' created.[/green]\n")
|
|
369
|
+
elif cr.status_code == 409:
|
|
370
|
+
# Race condition or namespace mismatch — package exists now
|
|
371
|
+
console.print(f" [dim]Package '{name}' already exists.[/dim]\n")
|
|
372
|
+
else:
|
|
373
|
+
console.print(
|
|
374
|
+
f" [red]Failed to create package ({cr.status_code}):[/red] {cr.text}\n"
|
|
375
|
+
f" You can create it manually at {REGISTRY_URL} or via:\n"
|
|
376
|
+
f" curl -X POST {REGISTRY_URL}/api/v1/packages \\\n"
|
|
377
|
+
f" -H 'X-API-Key: <token>' \\\n"
|
|
378
|
+
f" -H 'Content-Type: application/json' \\\n"
|
|
379
|
+
f' -d \'{{"Name": "{name}"}}\''
|
|
380
|
+
)
|
|
381
|
+
raise SystemExit(1)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def cmd_publish(args):
|
|
385
|
+
directory = getattr(args, "dir", ".")
|
|
386
|
+
skip_build = getattr(args, "skip_build", False)
|
|
387
|
+
dry_run = getattr(args, "dry_run", False)
|
|
388
|
+
path = Path(directory).resolve()
|
|
389
|
+
token = require_token()
|
|
390
|
+
|
|
391
|
+
# Read pyproject.toml for metadata
|
|
392
|
+
meta = _read_pyproject(path)
|
|
393
|
+
project = meta.get("project", {})
|
|
394
|
+
|
|
395
|
+
# Validate first
|
|
396
|
+
console.print("[dim]Running plugin validation...[/dim]")
|
|
397
|
+
try:
|
|
398
|
+
cmd_test(args)
|
|
399
|
+
except SystemExit as e:
|
|
400
|
+
if e.code and e.code != 0:
|
|
401
|
+
raise
|
|
402
|
+
|
|
403
|
+
# Build
|
|
404
|
+
if not skip_build:
|
|
405
|
+
console.print("\n[bold]Building...[/bold]")
|
|
406
|
+
import shutil
|
|
407
|
+
|
|
408
|
+
# Clean dist directory to avoid uploading stale files
|
|
409
|
+
dist_dir = path / "dist"
|
|
410
|
+
if dist_dir.exists():
|
|
411
|
+
shutil.rmtree(dist_dir)
|
|
412
|
+
|
|
413
|
+
# Try python -m build first, fall back to uv build
|
|
414
|
+
result = subprocess.run(
|
|
415
|
+
[sys.executable, "-m", "build", "--outdir", str(path / "dist")],
|
|
416
|
+
cwd=path,
|
|
417
|
+
capture_output=True,
|
|
418
|
+
text=True,
|
|
419
|
+
)
|
|
420
|
+
if result.returncode != 0:
|
|
421
|
+
# Fall back to uv build if available
|
|
422
|
+
uv_bin = shutil.which("uv")
|
|
423
|
+
if uv_bin:
|
|
424
|
+
console.print(
|
|
425
|
+
"[dim]python -m build unavailable, falling back to uv build...[/dim]"
|
|
426
|
+
)
|
|
427
|
+
result = subprocess.run(
|
|
428
|
+
[uv_bin, "build", "--out-dir", str(path / "dist")],
|
|
429
|
+
cwd=path,
|
|
430
|
+
)
|
|
431
|
+
if result.returncode != 0:
|
|
432
|
+
console.print("[red]Build failed.[/red]")
|
|
433
|
+
raise SystemExit(1)
|
|
434
|
+
else:
|
|
435
|
+
console.print(
|
|
436
|
+
f"[red]Build failed.[/red]\n"
|
|
437
|
+
f"[dim]{result.stderr.strip()}[/dim]\n"
|
|
438
|
+
"Install the build package: [bold]pip install build[/bold] "
|
|
439
|
+
"or use [bold]uv build[/bold]."
|
|
440
|
+
)
|
|
441
|
+
raise SystemExit(1)
|
|
442
|
+
|
|
443
|
+
dist = path / "dist"
|
|
444
|
+
files = sorted(dist.glob("*.whl")) + sorted(dist.glob("*.tar.gz"))
|
|
445
|
+
if not files:
|
|
446
|
+
console.print(
|
|
447
|
+
"[red]No dist files found.[/red] "
|
|
448
|
+
"Run without --skip-build or run `python -m build` first."
|
|
449
|
+
)
|
|
450
|
+
raise SystemExit(1)
|
|
451
|
+
|
|
452
|
+
# Determine canonical package name + version from pyproject.toml
|
|
453
|
+
# (more reliable than parsing filenames)
|
|
454
|
+
pkg_name = project.get("name", "").lower().replace("_", "-")
|
|
455
|
+
pkg_version = project.get("version", "")
|
|
456
|
+
|
|
457
|
+
# Fallback: parse from first wheel filename if pyproject metadata missing
|
|
458
|
+
if not pkg_name or not pkg_version:
|
|
459
|
+
stem = files[0].stem
|
|
460
|
+
parts = stem.split("-")
|
|
461
|
+
if not pkg_name:
|
|
462
|
+
pkg_name = parts[0].replace("_", "-").lower()
|
|
463
|
+
if not pkg_version and len(parts) > 1:
|
|
464
|
+
pkg_version = parts[1]
|
|
465
|
+
|
|
466
|
+
if dry_run:
|
|
467
|
+
console.print(
|
|
468
|
+
f"\n[bold]Dry run — would upload as {pkg_name}@{pkg_version}:[/bold]"
|
|
469
|
+
)
|
|
470
|
+
for f in files:
|
|
471
|
+
console.print(f" {f.name}")
|
|
472
|
+
return 0
|
|
473
|
+
|
|
474
|
+
client = RegistryClient(token)
|
|
475
|
+
|
|
476
|
+
# Ensure package exists on registry (auto-create if needed)
|
|
477
|
+
_ensure_package_exists(client, pkg_name, meta)
|
|
478
|
+
|
|
479
|
+
for file in files:
|
|
480
|
+
console.print(f"\n[bold]Uploading[/bold] {file.name}...")
|
|
481
|
+
|
|
482
|
+
with open(file, "rb") as fh:
|
|
483
|
+
try:
|
|
484
|
+
r = httpx.post(
|
|
485
|
+
f"{REGISTRY_URL}/api/v1/packages/{pkg_name}/upload",
|
|
486
|
+
headers={"X-API-Key": token},
|
|
487
|
+
files={"file": (file.name, fh, "application/octet-stream")},
|
|
488
|
+
data={
|
|
489
|
+
"name": pkg_name,
|
|
490
|
+
"version": pkg_version,
|
|
491
|
+
},
|
|
492
|
+
timeout=120,
|
|
493
|
+
)
|
|
494
|
+
except httpx.TimeoutException:
|
|
495
|
+
console.print("[red]Upload timed out.[/red]")
|
|
496
|
+
raise SystemExit(1)
|
|
497
|
+
|
|
498
|
+
if r.status_code == 201:
|
|
499
|
+
console.print(f" [green]✓ {file.name} uploaded successfully.[/green]")
|
|
500
|
+
elif r.status_code == 401:
|
|
501
|
+
console.print(
|
|
502
|
+
"[red]Invalid API key.[/red] Run [bold]devsper reg login[/bold] again."
|
|
503
|
+
)
|
|
504
|
+
raise SystemExit(1)
|
|
505
|
+
elif r.status_code == 404:
|
|
506
|
+
console.print(
|
|
507
|
+
f"[red]Package '{pkg_name}' not found on registry.[/red]\n"
|
|
508
|
+
f"This is unexpected — the package should have been created.\n"
|
|
509
|
+
f"Try creating it manually at {REGISTRY_URL}"
|
|
510
|
+
)
|
|
511
|
+
raise SystemExit(1)
|
|
512
|
+
elif r.status_code == 409:
|
|
513
|
+
console.print(
|
|
514
|
+
f"[yellow]Version {pkg_version} already exists.[/yellow] "
|
|
515
|
+
"Bump version in pyproject.toml."
|
|
516
|
+
)
|
|
517
|
+
raise SystemExit(1)
|
|
518
|
+
else:
|
|
519
|
+
console.print(f"[red]Upload failed ({r.status_code}):[/red] {r.text}")
|
|
520
|
+
raise SystemExit(1)
|
|
521
|
+
|
|
522
|
+
# Check verification status for the version
|
|
523
|
+
console.print(f"\n[dim]Checking verification status...[/dim]")
|
|
524
|
+
poll_deadline = time.time() + 120
|
|
525
|
+
with Progress(
|
|
526
|
+
SpinnerColumn(),
|
|
527
|
+
TextColumn("[progress.description]{task.description}"),
|
|
528
|
+
console=console,
|
|
529
|
+
) as progress:
|
|
530
|
+
task = progress.add_task("Verifying...", total=None)
|
|
531
|
+
while time.time() < poll_deadline:
|
|
532
|
+
time.sleep(3)
|
|
533
|
+
try:
|
|
534
|
+
sr = client.get(
|
|
535
|
+
f"/api/v1/packages/{pkg_name}/versions/{pkg_version}/status"
|
|
536
|
+
)
|
|
537
|
+
except Exception:
|
|
538
|
+
continue
|
|
539
|
+
if sr.status_code != 200:
|
|
540
|
+
# Status endpoint may not exist yet; check the version directly
|
|
541
|
+
try:
|
|
542
|
+
vr = client.get(f"/api/v1/packages/{pkg_name}/{pkg_version}")
|
|
543
|
+
if vr.status_code == 200:
|
|
544
|
+
vdata = vr.json()
|
|
545
|
+
vstatus = vdata.get("verification_status", "")
|
|
546
|
+
if vstatus == "passed" or vdata.get("published"):
|
|
547
|
+
progress.stop()
|
|
548
|
+
console.print(
|
|
549
|
+
f"\n[green]✓ {pkg_name}@{pkg_version} published![/green]\n"
|
|
550
|
+
f" Install: [bold]pip install "
|
|
551
|
+
f"--index-url {REGISTRY_URL}/simple/ {pkg_name}[/bold]"
|
|
552
|
+
)
|
|
553
|
+
return 0
|
|
554
|
+
except Exception:
|
|
555
|
+
pass
|
|
556
|
+
continue
|
|
557
|
+
|
|
558
|
+
status_data = sr.json()
|
|
559
|
+
status = status_data.get("verification_status")
|
|
560
|
+
|
|
561
|
+
if status == "passed":
|
|
562
|
+
progress.stop()
|
|
563
|
+
tool_count = status_data.get("tool_count", 0)
|
|
564
|
+
console.print(
|
|
565
|
+
f"\n[green]✓ {pkg_name}@{pkg_version} published![/green] "
|
|
566
|
+
f"({tool_count} tool(s) registered)\n"
|
|
567
|
+
f" Install: [bold]pip install "
|
|
568
|
+
f"--index-url {REGISTRY_URL}/simple/ {pkg_name}[/bold]"
|
|
569
|
+
)
|
|
570
|
+
return 0
|
|
571
|
+
elif status == "failed":
|
|
572
|
+
progress.stop()
|
|
573
|
+
report = status_data.get("verification_report", {})
|
|
574
|
+
console.print(
|
|
575
|
+
f"\n[red]✗ Verification failed:[/red]\n"
|
|
576
|
+
f"{json.dumps(report, indent=2)}"
|
|
577
|
+
)
|
|
578
|
+
raise SystemExit(1)
|
|
579
|
+
else:
|
|
580
|
+
progress.update(
|
|
581
|
+
task,
|
|
582
|
+
description=f"Verifying... ({status or 'pending'})",
|
|
583
|
+
)
|
|
584
|
+
else:
|
|
585
|
+
progress.stop()
|
|
586
|
+
console.print(
|
|
587
|
+
"[yellow]Verification timed out.[/yellow] "
|
|
588
|
+
"The upload succeeded but verification is still running.\n"
|
|
589
|
+
"Check status: "
|
|
590
|
+
f"[bold]devsper reg versions {pkg_name}[/bold]"
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
return 0
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
# ── search ─────────────────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def cmd_search(args):
|
|
600
|
+
query = args.query
|
|
601
|
+
verified_only = getattr(args, "verified", False)
|
|
602
|
+
limit = getattr(args, "limit", 10)
|
|
603
|
+
|
|
604
|
+
client = RegistryClient()
|
|
605
|
+
params: dict = {"q": query, "limit": limit}
|
|
606
|
+
if verified_only:
|
|
607
|
+
params["verified"] = "true"
|
|
608
|
+
|
|
609
|
+
try:
|
|
610
|
+
r = client.get("/api/v1/search", params=params)
|
|
611
|
+
r.raise_for_status()
|
|
612
|
+
except Exception as e:
|
|
613
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
614
|
+
return 1
|
|
615
|
+
|
|
616
|
+
data = r.json()
|
|
617
|
+
packages = data.get("packages") or data.get("results") or []
|
|
618
|
+
if not packages:
|
|
619
|
+
console.print("[dim]No results.[/dim]")
|
|
620
|
+
return 0
|
|
621
|
+
|
|
622
|
+
table = Table(show_header=True, header_style="bold")
|
|
623
|
+
table.add_column("Package")
|
|
624
|
+
table.add_column("Version")
|
|
625
|
+
table.add_column("Downloads", justify="right")
|
|
626
|
+
table.add_column("", justify="center") # verified badge
|
|
627
|
+
for p in packages:
|
|
628
|
+
badge = "[green]✓[/green]" if p.get("verified") else ""
|
|
629
|
+
table.add_row(
|
|
630
|
+
p["name"],
|
|
631
|
+
p.get("latest_version") or "—",
|
|
632
|
+
str(p.get("total_downloads") or 0),
|
|
633
|
+
badge,
|
|
634
|
+
)
|
|
635
|
+
console.print(table)
|
|
636
|
+
return 0
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
# ── info ───────────────────────────────────────────────────────────────────
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def cmd_info(args):
|
|
643
|
+
name = args.package
|
|
644
|
+
client = RegistryClient()
|
|
645
|
+
try:
|
|
646
|
+
r = client.get(f"/api/v1/packages/{name}")
|
|
647
|
+
except Exception as e:
|
|
648
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
649
|
+
return 1
|
|
650
|
+
|
|
651
|
+
if r.status_code == 404:
|
|
652
|
+
console.print(f"[red]Package '{name}' not found.[/red]")
|
|
653
|
+
raise SystemExit(1)
|
|
654
|
+
r.raise_for_status()
|
|
655
|
+
|
|
656
|
+
d = r.json()
|
|
657
|
+
console.print(f"[bold]Name:[/bold] {d['name']}")
|
|
658
|
+
console.print(f"[bold]Description:[/bold] {d.get('description') or '—'}")
|
|
659
|
+
console.print(f"[bold]Latest:[/bold] {d.get('latest_version') or '—'}")
|
|
660
|
+
console.print(f"[bold]Downloads:[/bold] {d.get('total_downloads') or 0}")
|
|
661
|
+
console.print(f"[bold]Verified:[/bold] {'Yes' if d.get('verified') else 'No'}")
|
|
662
|
+
console.print(
|
|
663
|
+
f"[bold]Install:[/bold] pip install "
|
|
664
|
+
f"--index-url {REGISTRY_URL}/simple/ {name}"
|
|
665
|
+
)
|
|
666
|
+
return 0
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
# ── versions ───────────────────────────────────────────────────────────────
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def cmd_versions(args):
|
|
673
|
+
name = args.package
|
|
674
|
+
client = RegistryClient()
|
|
675
|
+
try:
|
|
676
|
+
r = client.get(f"/api/v1/packages/{name}/versions")
|
|
677
|
+
except Exception as e:
|
|
678
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
679
|
+
return 1
|
|
680
|
+
|
|
681
|
+
if r.status_code == 404:
|
|
682
|
+
console.print(f"[red]Package '{name}' not found.[/red]")
|
|
683
|
+
raise SystemExit(1)
|
|
684
|
+
r.raise_for_status()
|
|
685
|
+
|
|
686
|
+
versions = r.json().get("versions", [])
|
|
687
|
+
table = Table(show_header=True, header_style="bold")
|
|
688
|
+
table.add_column("Version")
|
|
689
|
+
table.add_column("Published")
|
|
690
|
+
table.add_column("Downloads", justify="right")
|
|
691
|
+
table.add_column("Status")
|
|
692
|
+
table.add_column("Yanked", justify="center")
|
|
693
|
+
for v in versions:
|
|
694
|
+
yanked = "[red]yanked[/red]" if v.get("yanked") else ""
|
|
695
|
+
uploaded = v.get("uploaded_at") or "—"
|
|
696
|
+
if uploaded != "—":
|
|
697
|
+
uploaded = uploaded[:10]
|
|
698
|
+
table.add_row(
|
|
699
|
+
v["version"],
|
|
700
|
+
uploaded,
|
|
701
|
+
str(v.get("download_count") or 0),
|
|
702
|
+
v.get("verification_status") or "—",
|
|
703
|
+
yanked,
|
|
704
|
+
)
|
|
705
|
+
console.print(table)
|
|
706
|
+
return 0
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
# ── yank ───────────────────────────────────────────────────────────────────
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def cmd_yank(args):
|
|
713
|
+
name = args.package
|
|
714
|
+
version = args.version
|
|
715
|
+
reason = args.reason
|
|
716
|
+
token = require_token()
|
|
717
|
+
client = RegistryClient(token)
|
|
718
|
+
|
|
719
|
+
try:
|
|
720
|
+
r = client.post(
|
|
721
|
+
f"/api/v1/packages/{name}/{version}/yank",
|
|
722
|
+
json={"reason": reason},
|
|
723
|
+
)
|
|
724
|
+
except Exception as e:
|
|
725
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
726
|
+
return 1
|
|
727
|
+
|
|
728
|
+
if r.status_code == 401:
|
|
729
|
+
console.print("[red]Not authorized.[/red]")
|
|
730
|
+
raise SystemExit(1)
|
|
731
|
+
r.raise_for_status()
|
|
732
|
+
console.print(f"[green]✓ Yanked {name}@{version}.[/green]")
|
|
733
|
+
return 0
|