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.
Files changed (375) hide show
  1. devsper/__init__.py +14 -0
  2. devsper/agents/a2a/__init__.py +27 -0
  3. devsper/agents/a2a/client.py +126 -0
  4. devsper/agents/a2a/discovery.py +24 -0
  5. devsper/agents/a2a/server.py +128 -0
  6. devsper/agents/a2a/tool_adapter.py +68 -0
  7. devsper/agents/a2a/types.py +49 -0
  8. devsper/agents/agent.py +602 -0
  9. devsper/agents/critic.py +80 -0
  10. devsper/agents/message_bus.py +124 -0
  11. devsper/agents/roles.py +181 -0
  12. devsper/agents/run_agent.py +78 -0
  13. devsper/analytics/__init__.py +5 -0
  14. devsper/analytics/tool_analytics.py +78 -0
  15. devsper/audit/__init__.py +5 -0
  16. devsper/audit/logger.py +214 -0
  17. devsper/bus/__init__.py +29 -0
  18. devsper/bus/backends/__init__.py +5 -0
  19. devsper/bus/backends/base.py +38 -0
  20. devsper/bus/backends/memory.py +55 -0
  21. devsper/bus/backends/redis.py +146 -0
  22. devsper/bus/message.py +56 -0
  23. devsper/bus/schema_version.py +3 -0
  24. devsper/bus/topics.py +19 -0
  25. devsper/cache/__init__.py +6 -0
  26. devsper/cache/embedding_index.py +98 -0
  27. devsper/cache/hashing.py +24 -0
  28. devsper/cache/store.py +153 -0
  29. devsper/cache/task_cache.py +191 -0
  30. devsper/cli/__init__.py +6 -0
  31. devsper/cli/commands/reg.py +733 -0
  32. devsper/cli/github_oauth.py +157 -0
  33. devsper/cli/init.py +637 -0
  34. devsper/cli/main.py +2956 -0
  35. devsper/cli/run_progress.py +103 -0
  36. devsper/cli/ui/__init__.py +65 -0
  37. devsper/cli/ui/components.py +94 -0
  38. devsper/cli/ui/errors.py +104 -0
  39. devsper/cli/ui/logging.py +120 -0
  40. devsper/cli/ui/onboarding.py +102 -0
  41. devsper/cli/ui/progress.py +43 -0
  42. devsper/cli/ui/run_view.py +308 -0
  43. devsper/cli/ui/theme.py +40 -0
  44. devsper/cluster/__init__.py +29 -0
  45. devsper/cluster/election.py +84 -0
  46. devsper/cluster/local.py +97 -0
  47. devsper/cluster/node_info.py +77 -0
  48. devsper/cluster/registry.py +71 -0
  49. devsper/cluster/router.py +117 -0
  50. devsper/cluster/state_backend.py +105 -0
  51. devsper/compliance/__init__.py +5 -0
  52. devsper/compliance/pii.py +147 -0
  53. devsper/config/__init__.py +52 -0
  54. devsper/config/config_loader.py +121 -0
  55. devsper/config/defaults.py +77 -0
  56. devsper/config/resolver.py +342 -0
  57. devsper/config/schema.py +237 -0
  58. devsper/credentials/__init__.py +19 -0
  59. devsper/credentials/cli.py +197 -0
  60. devsper/credentials/migration.py +124 -0
  61. devsper/credentials/store.py +142 -0
  62. devsper/dashboard/__init__.py +9 -0
  63. devsper/dashboard/dashboard.py +87 -0
  64. devsper/dev/__init__.py +25 -0
  65. devsper/dev/builder.py +195 -0
  66. devsper/dev/debugger.py +95 -0
  67. devsper/dev/repo_index.py +138 -0
  68. devsper/dev/sandbox.py +203 -0
  69. devsper/dev/scaffold.py +122 -0
  70. devsper/embeddings/__init__.py +5 -0
  71. devsper/embeddings/service.py +36 -0
  72. devsper/explainability/__init__.py +14 -0
  73. devsper/explainability/decision_tree.py +104 -0
  74. devsper/explainability/rationale.py +38 -0
  75. devsper/explainability/simulation.py +56 -0
  76. devsper/hitl/__init__.py +13 -0
  77. devsper/hitl/approval.py +160 -0
  78. devsper/hitl/escalation.py +95 -0
  79. devsper/intelligence/__init__.py +9 -0
  80. devsper/intelligence/adaptation.py +88 -0
  81. devsper/intelligence/analysis/__init__.py +19 -0
  82. devsper/intelligence/analysis/analyzer.py +71 -0
  83. devsper/intelligence/analysis/cost_estimator.py +66 -0
  84. devsper/intelligence/analysis/formatter.py +103 -0
  85. devsper/intelligence/analysis/run_report.py +402 -0
  86. devsper/intelligence/learning_engine.py +92 -0
  87. devsper/intelligence/strategies/__init__.py +23 -0
  88. devsper/intelligence/strategies/base.py +14 -0
  89. devsper/intelligence/strategies/code_analysis_strategy.py +33 -0
  90. devsper/intelligence/strategies/data_science_strategy.py +33 -0
  91. devsper/intelligence/strategies/document_pipeline_strategy.py +33 -0
  92. devsper/intelligence/strategies/experiment_strategy.py +33 -0
  93. devsper/intelligence/strategies/research_strategy.py +34 -0
  94. devsper/intelligence/strategy_selector.py +84 -0
  95. devsper/intelligence/synthesis.py +132 -0
  96. devsper/intelligence/task_optimizer.py +92 -0
  97. devsper/knowledge/__init__.py +5 -0
  98. devsper/knowledge/extractor.py +204 -0
  99. devsper/knowledge/knowledge_graph.py +184 -0
  100. devsper/knowledge/query.py +285 -0
  101. devsper/memory/__init__.py +35 -0
  102. devsper/memory/consolidation.py +138 -0
  103. devsper/memory/embeddings.py +60 -0
  104. devsper/memory/memory_index.py +97 -0
  105. devsper/memory/memory_router.py +62 -0
  106. devsper/memory/memory_store.py +221 -0
  107. devsper/memory/memory_types.py +54 -0
  108. devsper/memory/namespaces.py +45 -0
  109. devsper/memory/scoring.py +77 -0
  110. devsper/memory/summarizer.py +52 -0
  111. devsper/nodes/__init__.py +5 -0
  112. devsper/nodes/controller.py +449 -0
  113. devsper/nodes/rpc.py +127 -0
  114. devsper/nodes/single.py +161 -0
  115. devsper/nodes/worker.py +506 -0
  116. devsper/orchestration/__init__.py +19 -0
  117. devsper/orchestration/meta_planner.py +239 -0
  118. devsper/orchestration/priority_queue.py +61 -0
  119. devsper/plugins/__init__.py +19 -0
  120. devsper/plugins/marketplace/__init__.py +0 -0
  121. devsper/plugins/plugin_loader.py +70 -0
  122. devsper/plugins/plugin_registry.py +34 -0
  123. devsper/plugins/registry.py +83 -0
  124. devsper/protocols/__init__.py +6 -0
  125. devsper/providers/__init__.py +17 -0
  126. devsper/providers/anthropic.py +84 -0
  127. devsper/providers/base.py +75 -0
  128. devsper/providers/complexity_router.py +94 -0
  129. devsper/providers/gemini.py +36 -0
  130. devsper/providers/github.py +180 -0
  131. devsper/providers/model_router.py +40 -0
  132. devsper/providers/openai.py +105 -0
  133. devsper/providers/router/__init__.py +21 -0
  134. devsper/providers/router/backends/__init__.py +19 -0
  135. devsper/providers/router/backends/anthropic_backend.py +111 -0
  136. devsper/providers/router/backends/custom_backend.py +138 -0
  137. devsper/providers/router/backends/gemini_backend.py +89 -0
  138. devsper/providers/router/backends/github_backend.py +165 -0
  139. devsper/providers/router/backends/ollama_backend.py +104 -0
  140. devsper/providers/router/backends/openai_backend.py +142 -0
  141. devsper/providers/router/backends/vllm_backend.py +35 -0
  142. devsper/providers/router/base.py +60 -0
  143. devsper/providers/router/factory.py +92 -0
  144. devsper/providers/router/legacy.py +101 -0
  145. devsper/providers/router/router.py +135 -0
  146. devsper/reasoning/__init__.py +12 -0
  147. devsper/reasoning/graph.py +59 -0
  148. devsper/reasoning/nodes.py +20 -0
  149. devsper/reasoning/store.py +67 -0
  150. devsper/runtime/__init__.py +12 -0
  151. devsper/runtime/health.py +88 -0
  152. devsper/runtime/replay.py +53 -0
  153. devsper/runtime/replay_engine.py +142 -0
  154. devsper/runtime/run_history.py +204 -0
  155. devsper/runtime/telemetry.py +116 -0
  156. devsper/runtime/visualize.py +58 -0
  157. devsper/sandbox/__init__.py +13 -0
  158. devsper/sandbox/sandbox.py +161 -0
  159. devsper/swarm/checkpointer.py +65 -0
  160. devsper/swarm/executor.py +558 -0
  161. devsper/swarm/map_reduce.py +44 -0
  162. devsper/swarm/planner.py +197 -0
  163. devsper/swarm/prefetcher.py +91 -0
  164. devsper/swarm/scheduler.py +153 -0
  165. devsper/swarm/speculation.py +47 -0
  166. devsper/swarm/swarm.py +562 -0
  167. devsper/tools/__init__.py +33 -0
  168. devsper/tools/base.py +29 -0
  169. devsper/tools/code_intelligence/__init__.py +13 -0
  170. devsper/tools/code_intelligence/api_surface_extractor.py +73 -0
  171. devsper/tools/code_intelligence/architecture_analyzer.py +65 -0
  172. devsper/tools/code_intelligence/codebase_indexer.py +71 -0
  173. devsper/tools/code_intelligence/dependency_graph_builder.py +67 -0
  174. devsper/tools/code_intelligence/design_pattern_detector.py +62 -0
  175. devsper/tools/code_intelligence/large_function_detector.py +68 -0
  176. devsper/tools/code_intelligence/module_responsibility_mapper.py +56 -0
  177. devsper/tools/code_intelligence/parallel_codebase_analysis.py +44 -0
  178. devsper/tools/code_intelligence/refactor_candidate_detector.py +81 -0
  179. devsper/tools/code_intelligence/repository_semantic_index.py +61 -0
  180. devsper/tools/code_intelligence/test_coverage_estimator.py +62 -0
  181. devsper/tools/coding/__init__.py +12 -0
  182. devsper/tools/coding/analyze_code_complexity.py +48 -0
  183. devsper/tools/coding/dependency_analyzer.py +42 -0
  184. devsper/tools/coding/extract_functions.py +38 -0
  185. devsper/tools/coding/format_python.py +50 -0
  186. devsper/tools/coding/generate_docstrings.py +40 -0
  187. devsper/tools/coding/generate_unit_tests.py +42 -0
  188. devsper/tools/coding/lint_python.py +51 -0
  189. devsper/tools/coding/refactor_function.py +41 -0
  190. devsper/tools/coding/repo_structure_map.py +54 -0
  191. devsper/tools/coding/run_python.py +53 -0
  192. devsper/tools/data/__init__.py +12 -0
  193. devsper/tools/data/column_type_detection.py +64 -0
  194. devsper/tools/data/csv_summary.py +52 -0
  195. devsper/tools/data/dataframe_filter.py +51 -0
  196. devsper/tools/data/dataframe_groupby.py +47 -0
  197. devsper/tools/data/dataframe_stats.py +38 -0
  198. devsper/tools/data/dataset_sampling.py +55 -0
  199. devsper/tools/data/dataset_schema.py +45 -0
  200. devsper/tools/data/json_pretty_print.py +37 -0
  201. devsper/tools/data/json_query.py +46 -0
  202. devsper/tools/data/missing_value_report.py +47 -0
  203. devsper/tools/data_science/__init__.py +13 -0
  204. devsper/tools/data_science/correlation_heatmap.py +72 -0
  205. devsper/tools/data_science/dataset_bias_detector.py +49 -0
  206. devsper/tools/data_science/dataset_distribution_report.py +64 -0
  207. devsper/tools/data_science/dataset_drift_detector.py +64 -0
  208. devsper/tools/data_science/dataset_outlier_detector.py +65 -0
  209. devsper/tools/data_science/dataset_profile.py +76 -0
  210. devsper/tools/data_science/distributed_dataset_processor.py +54 -0
  211. devsper/tools/data_science/feature_engineering_suggestions.py +69 -0
  212. devsper/tools/data_science/feature_importance_estimator.py +82 -0
  213. devsper/tools/data_science/model_input_validator.py +59 -0
  214. devsper/tools/data_science/time_series_analyzer.py +57 -0
  215. devsper/tools/documents/__init__.py +11 -0
  216. devsper/tools/documents/_docproc.py +56 -0
  217. devsper/tools/documents/document_to_markdown.py +29 -0
  218. devsper/tools/documents/extract_document_images.py +39 -0
  219. devsper/tools/documents/extract_document_text.py +29 -0
  220. devsper/tools/documents/extract_equations.py +36 -0
  221. devsper/tools/documents/extract_tables.py +47 -0
  222. devsper/tools/documents/summarize_document.py +42 -0
  223. devsper/tools/documents/write_latex_document.py +133 -0
  224. devsper/tools/documents/write_markdown_document.py +89 -0
  225. devsper/tools/documents/write_word_document.py +149 -0
  226. devsper/tools/experiments/__init__.py +13 -0
  227. devsper/tools/experiments/bootstrap_estimator.py +54 -0
  228. devsper/tools/experiments/experiment_report_generator.py +50 -0
  229. devsper/tools/experiments/experiment_tracker.py +36 -0
  230. devsper/tools/experiments/grid_search_runner.py +50 -0
  231. devsper/tools/experiments/model_benchmark_runner.py +45 -0
  232. devsper/tools/experiments/monte_carlo_experiment.py +38 -0
  233. devsper/tools/experiments/parameter_sweep_runner.py +51 -0
  234. devsper/tools/experiments/result_comparator.py +58 -0
  235. devsper/tools/experiments/simulation_runner.py +43 -0
  236. devsper/tools/experiments/statistical_significance_test.py +56 -0
  237. devsper/tools/experiments/swarm_map_reduce.py +42 -0
  238. devsper/tools/filesystem/__init__.py +12 -0
  239. devsper/tools/filesystem/append_file.py +42 -0
  240. devsper/tools/filesystem/file_hash.py +40 -0
  241. devsper/tools/filesystem/file_line_count.py +36 -0
  242. devsper/tools/filesystem/file_metadata.py +38 -0
  243. devsper/tools/filesystem/file_preview.py +55 -0
  244. devsper/tools/filesystem/find_large_files.py +50 -0
  245. devsper/tools/filesystem/list_directory.py +39 -0
  246. devsper/tools/filesystem/read_file.py +35 -0
  247. devsper/tools/filesystem/search_files.py +60 -0
  248. devsper/tools/filesystem/write_file.py +41 -0
  249. devsper/tools/flagship/__init__.py +15 -0
  250. devsper/tools/flagship/distributed_document_analysis.py +77 -0
  251. devsper/tools/flagship/docproc_corpus_pipeline.py +91 -0
  252. devsper/tools/flagship/repository_semantic_map.py +99 -0
  253. devsper/tools/flagship/research_graph_builder.py +111 -0
  254. devsper/tools/flagship/swarm_experiment_runner.py +86 -0
  255. devsper/tools/knowledge/__init__.py +10 -0
  256. devsper/tools/knowledge/citation_graph_builder.py +69 -0
  257. devsper/tools/knowledge/concept_frequency_analyzer.py +74 -0
  258. devsper/tools/knowledge/corpus_builder.py +66 -0
  259. devsper/tools/knowledge/cross_document_entity_linker.py +71 -0
  260. devsper/tools/knowledge/document_corpus_summary.py +68 -0
  261. devsper/tools/knowledge/document_topic_extractor.py +58 -0
  262. devsper/tools/knowledge/knowledge_graph_extractor.py +58 -0
  263. devsper/tools/knowledge/timeline_extractor.py +59 -0
  264. devsper/tools/math/__init__.py +12 -0
  265. devsper/tools/math/calculate_expression.py +52 -0
  266. devsper/tools/math/correlation.py +44 -0
  267. devsper/tools/math/distribution_summary.py +39 -0
  268. devsper/tools/math/histogram.py +53 -0
  269. devsper/tools/math/linear_regression.py +47 -0
  270. devsper/tools/math/matrix_multiply.py +38 -0
  271. devsper/tools/math/mean_std.py +35 -0
  272. devsper/tools/math/monte_carlo_simulation.py +43 -0
  273. devsper/tools/math/polynomial_fit.py +40 -0
  274. devsper/tools/math/random_sample.py +36 -0
  275. devsper/tools/mcp/__init__.py +23 -0
  276. devsper/tools/mcp/adapter.py +53 -0
  277. devsper/tools/mcp/client.py +235 -0
  278. devsper/tools/mcp/discovery.py +53 -0
  279. devsper/tools/memory/__init__.py +16 -0
  280. devsper/tools/memory/delete_memory.py +25 -0
  281. devsper/tools/memory/list_memory.py +34 -0
  282. devsper/tools/memory/search_memory.py +36 -0
  283. devsper/tools/memory/store_memory.py +47 -0
  284. devsper/tools/memory/summarize_memory.py +41 -0
  285. devsper/tools/memory/tag_memory.py +47 -0
  286. devsper/tools/pipelines.py +92 -0
  287. devsper/tools/registry.py +39 -0
  288. devsper/tools/research/__init__.py +12 -0
  289. devsper/tools/research/arxiv_download.py +55 -0
  290. devsper/tools/research/arxiv_search.py +58 -0
  291. devsper/tools/research/citation_extractor.py +35 -0
  292. devsper/tools/research/duckduckgo_search.py +42 -0
  293. devsper/tools/research/paper_metadata_extractor.py +45 -0
  294. devsper/tools/research/paper_summarizer.py +41 -0
  295. devsper/tools/research/research_question_generator.py +39 -0
  296. devsper/tools/research/topic_cluster.py +46 -0
  297. devsper/tools/research/web_search.py +47 -0
  298. devsper/tools/research/wikipedia_lookup.py +50 -0
  299. devsper/tools/research_advanced/__init__.py +14 -0
  300. devsper/tools/research_advanced/citation_context_extractor.py +60 -0
  301. devsper/tools/research_advanced/literature_review_generator.py +79 -0
  302. devsper/tools/research_advanced/methodology_extractor.py +58 -0
  303. devsper/tools/research_advanced/paper_contribution_extractor.py +50 -0
  304. devsper/tools/research_advanced/paper_dataset_identifier.py +49 -0
  305. devsper/tools/research_advanced/paper_method_comparator.py +62 -0
  306. devsper/tools/research_advanced/paper_similarity_search.py +69 -0
  307. devsper/tools/research_advanced/paper_trend_analyzer.py +69 -0
  308. devsper/tools/research_advanced/parallel_document_analyzer.py +56 -0
  309. devsper/tools/research_advanced/research_gap_finder.py +71 -0
  310. devsper/tools/research_advanced/research_topic_mapper.py +69 -0
  311. devsper/tools/research_advanced/swarm_literature_review.py +58 -0
  312. devsper/tools/scoring/__init__.py +52 -0
  313. devsper/tools/scoring/report.py +44 -0
  314. devsper/tools/scoring/scorer.py +39 -0
  315. devsper/tools/scoring/selector.py +61 -0
  316. devsper/tools/scoring/store.py +267 -0
  317. devsper/tools/selector.py +130 -0
  318. devsper/tools/system/__init__.py +12 -0
  319. devsper/tools/system/cpu_usage.py +22 -0
  320. devsper/tools/system/disk_usage.py +35 -0
  321. devsper/tools/system/environment_variables.py +29 -0
  322. devsper/tools/system/memory_usage.py +23 -0
  323. devsper/tools/system/pip_install.py +44 -0
  324. devsper/tools/system/pip_search.py +29 -0
  325. devsper/tools/system/process_list.py +34 -0
  326. devsper/tools/system/python_package_list.py +40 -0
  327. devsper/tools/system/run_shell_command.py +51 -0
  328. devsper/tools/system/system_info.py +26 -0
  329. devsper/tools/tool_runner.py +122 -0
  330. devsper/tui/__init__.py +5 -0
  331. devsper/tui/activity_feed_view.py +73 -0
  332. devsper/tui/adaptive_tasks_view.py +75 -0
  333. devsper/tui/agent_role_view.py +35 -0
  334. devsper/tui/app.py +395 -0
  335. devsper/tui/dashboard_screen.py +290 -0
  336. devsper/tui/dev_view.py +99 -0
  337. devsper/tui/inject_screen.py +73 -0
  338. devsper/tui/knowledge_graph_view.py +46 -0
  339. devsper/tui/layout.py +43 -0
  340. devsper/tui/logs_view.py +83 -0
  341. devsper/tui/memory_view.py +58 -0
  342. devsper/tui/performance_view.py +33 -0
  343. devsper/tui/reasoning_graph_view.py +39 -0
  344. devsper/tui/results_view.py +139 -0
  345. devsper/tui/swarm_view.py +37 -0
  346. devsper/tui/task_detail_screen.py +55 -0
  347. devsper/tui/task_view.py +103 -0
  348. devsper/types/event.py +97 -0
  349. devsper/types/exceptions.py +21 -0
  350. devsper/types/swarm.py +41 -0
  351. devsper/types/task.py +80 -0
  352. devsper/upgrade/__init__.py +21 -0
  353. devsper/upgrade/changelog.py +124 -0
  354. devsper/upgrade/cli.py +145 -0
  355. devsper/upgrade/installer.py +103 -0
  356. devsper/upgrade/notifier.py +52 -0
  357. devsper/upgrade/version_check.py +121 -0
  358. devsper/utils/event_logger.py +88 -0
  359. devsper/utils/http.py +43 -0
  360. devsper/utils/models.py +54 -0
  361. devsper/visualization/__init__.py +5 -0
  362. devsper/visualization/dag_export.py +67 -0
  363. devsper/workflow/__init__.py +18 -0
  364. devsper/workflow/conditions.py +157 -0
  365. devsper/workflow/context.py +108 -0
  366. devsper/workflow/loader.py +156 -0
  367. devsper/workflow/resolver.py +109 -0
  368. devsper/workflow/runner.py +562 -0
  369. devsper/workflow/schema.py +63 -0
  370. devsper/workflow/validator.py +128 -0
  371. devsper-2.1.6.dist-info/METADATA +346 -0
  372. devsper-2.1.6.dist-info/RECORD +375 -0
  373. devsper-2.1.6.dist-info/WHEEL +4 -0
  374. devsper-2.1.6.dist-info/entry_points.txt +3 -0
  375. 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