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,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task detail overlay: full description, result, tools, duration, retry count, error.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Container, ScrollableContainer
|
|
7
|
+
from textual.screen import ModalScreen
|
|
8
|
+
from textual.widgets import Button, Static
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskDetailScreen(ModalScreen[None]):
|
|
12
|
+
"""Shows full task details. Receives task dict with task_id, description, result, status, error, role, etc."""
|
|
13
|
+
|
|
14
|
+
BINDINGS = [("escape", "dismiss_screen")]
|
|
15
|
+
|
|
16
|
+
def __init__(self, task: dict, app_ref: object) -> None:
|
|
17
|
+
super().__init__()
|
|
18
|
+
self._task = task
|
|
19
|
+
self._app_ref = app_ref
|
|
20
|
+
|
|
21
|
+
def compose(self) -> ComposeResult:
|
|
22
|
+
t = self._task
|
|
23
|
+
task_id = t.get("task_id", "?")
|
|
24
|
+
desc = (t.get("description") or "(no description)").replace("\n", " ")
|
|
25
|
+
result = (t.get("result") or "(no result)")[:8000]
|
|
26
|
+
status = t.get("status", "?")
|
|
27
|
+
error = t.get("error") or ""
|
|
28
|
+
role = t.get("role") or "—"
|
|
29
|
+
runtime = t.get("runtime") or "—"
|
|
30
|
+
tools_used = t.get("tools_used") or []
|
|
31
|
+
tools_str = ", ".join(tools_used) if tools_used else "—"
|
|
32
|
+
retry = t.get("retry_count", 0)
|
|
33
|
+
lines = [
|
|
34
|
+
f"[bold]Task ID:[/] {task_id}",
|
|
35
|
+
f"[bold]Description:[/] {desc}",
|
|
36
|
+
f"[bold]Role:[/] {role} [bold]Status:[/] {status} [bold]Runtime:[/] {runtime}",
|
|
37
|
+
f"[bold]Retry count:[/] {retry}",
|
|
38
|
+
f"[bold]Tools used:[/] {tools_str}",
|
|
39
|
+
"",
|
|
40
|
+
"[bold]Result:[/]",
|
|
41
|
+
result,
|
|
42
|
+
]
|
|
43
|
+
if error:
|
|
44
|
+
lines.extend(["", "[bold red]Error:[/]", error])
|
|
45
|
+
content = "\n".join(lines)
|
|
46
|
+
with Container(id="task-detail-container"):
|
|
47
|
+
yield ScrollableContainer(Static(content, id="task-detail-content"))
|
|
48
|
+
yield Button("Close", variant="primary", id="task-detail-close")
|
|
49
|
+
|
|
50
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
51
|
+
if event.button.id == "task-detail-close":
|
|
52
|
+
self.dismiss(None)
|
|
53
|
+
|
|
54
|
+
def action_dismiss_screen(self) -> None:
|
|
55
|
+
self.dismiss(None)
|
devsper/tui/task_view.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task panel: task_id, status, runtime, worker agent.
|
|
3
|
+
|
|
4
|
+
Data can come from last scheduler or from event stream (task_started, task_completed).
|
|
5
|
+
Arrow-key selection and Enter opens task detail overlay.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from textual.widgets import Static, ListView, ListItem, Label
|
|
9
|
+
from textual.reactive import reactive
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskView(Static):
|
|
14
|
+
"""Displays list of tasks with status; select with arrows, Enter for detail."""
|
|
15
|
+
|
|
16
|
+
tasks_data: reactive[list[dict]] = reactive(list)
|
|
17
|
+
|
|
18
|
+
class TaskSelected(Message):
|
|
19
|
+
"""Emitted when user presses Enter on a selected task."""
|
|
20
|
+
def __init__(self, task: dict) -> None:
|
|
21
|
+
self.task = task
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
25
|
+
super().__init__(*args, **kwargs)
|
|
26
|
+
self._tasks: list[dict] = []
|
|
27
|
+
self._list_view: ListView | None = None
|
|
28
|
+
|
|
29
|
+
def compose(self):
|
|
30
|
+
from textual.containers import VerticalScroll
|
|
31
|
+
with VerticalScroll():
|
|
32
|
+
self._list_view = ListView(id="task-list-view")
|
|
33
|
+
yield self._list_view
|
|
34
|
+
|
|
35
|
+
def set_tasks(self, tasks: list[dict]) -> None:
|
|
36
|
+
"""Update tasks. Each dict: task_id, status, runtime (str), worker (str), and optionally description, result, error."""
|
|
37
|
+
self._tasks = tasks
|
|
38
|
+
self.tasks_data = tasks
|
|
39
|
+
self._refresh_list()
|
|
40
|
+
|
|
41
|
+
def add_or_update_task(
|
|
42
|
+
self, task_id: str, status: str, runtime: str = "-", worker: str = "agent"
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Add or update a single task (e.g. from events)."""
|
|
45
|
+
for t in self._tasks:
|
|
46
|
+
if t.get("task_id") == task_id:
|
|
47
|
+
t["status"] = status
|
|
48
|
+
t["runtime"] = runtime
|
|
49
|
+
t["worker"] = worker
|
|
50
|
+
self.tasks_data = list(self._tasks)
|
|
51
|
+
self._refresh_list()
|
|
52
|
+
return
|
|
53
|
+
self._tasks.append(
|
|
54
|
+
{
|
|
55
|
+
"task_id": task_id,
|
|
56
|
+
"status": status,
|
|
57
|
+
"runtime": runtime,
|
|
58
|
+
"worker": worker,
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
self.tasks_data = list(self._tasks)
|
|
62
|
+
self._refresh_list()
|
|
63
|
+
|
|
64
|
+
def watch_tasks_data(self, data: list[dict]) -> None:
|
|
65
|
+
self._tasks = data
|
|
66
|
+
self._refresh_list()
|
|
67
|
+
|
|
68
|
+
def _refresh_list(self) -> None:
|
|
69
|
+
try:
|
|
70
|
+
lv = self.query_one("#task-list-view", ListView)
|
|
71
|
+
except Exception:
|
|
72
|
+
return
|
|
73
|
+
try:
|
|
74
|
+
lv.clear_children()
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
if not self._tasks:
|
|
78
|
+
return
|
|
79
|
+
for t in self._tasks:
|
|
80
|
+
tid = t.get("task_id", "?")
|
|
81
|
+
status = t.get("status", "pending")
|
|
82
|
+
desc = (t.get("description") or tid)[:50]
|
|
83
|
+
item = ListItem(Label(f"{tid} {status} {desc}"), id=f"task-{tid}")
|
|
84
|
+
lv.append(item)
|
|
85
|
+
|
|
86
|
+
def get_selected_task(self) -> dict | None:
|
|
87
|
+
"""Return the currently selected task dict, or None."""
|
|
88
|
+
try:
|
|
89
|
+
lv = self.query_one("#task-list-view", ListView)
|
|
90
|
+
idx = lv.index
|
|
91
|
+
if idx is not None and 0 <= idx < len(self._tasks):
|
|
92
|
+
return self._tasks[idx]
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
98
|
+
idx = getattr(event.list_view, "index", None)
|
|
99
|
+
if idx is not None and 0 <= idx < len(self._tasks):
|
|
100
|
+
self.post_message(self.TaskSelected(self._tasks[idx]))
|
|
101
|
+
|
|
102
|
+
def on_mount(self) -> None:
|
|
103
|
+
self._refresh_list()
|
devsper/types/event.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pydantic import BaseModel, model_validator
|
|
5
|
+
|
|
6
|
+
from devsper.types.exceptions import EventSerializationError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class events(Enum):
|
|
10
|
+
SWARM_STARTED = "swarm_started"
|
|
11
|
+
SWARM_FINISHED = "swarm_finished"
|
|
12
|
+
TASK_CREATED = "task_created"
|
|
13
|
+
TASK_STARTED = "task_started"
|
|
14
|
+
TASK_COMPLETED = "task_completed"
|
|
15
|
+
TASK_FAILED = "task_failed"
|
|
16
|
+
TASK_CACHE_HIT = "task_cache_hit" # v1.6: payload task_id, similarity, original_description
|
|
17
|
+
TASK_CACHE_MISS = "task_cache_miss" # v1.6: payload task_id
|
|
18
|
+
TASK_MODEL_SELECTED = "task_model_selected" # v1.6: payload task_id, tier, model
|
|
19
|
+
AGENT_STARTED = "agent_started"
|
|
20
|
+
AGENT_FINISHED = "agent_finished"
|
|
21
|
+
PLANNER_STARTED = "planner_started"
|
|
22
|
+
PLANNER_FINISHED = "planner_finished"
|
|
23
|
+
EXECUTOR_STARTED = "executor_started"
|
|
24
|
+
EXECUTOR_FINISHED = "executor_finished"
|
|
25
|
+
TOOL_CALLED = "tool_called"
|
|
26
|
+
REASONING_NODE_ADDED = "reasoning_node_added"
|
|
27
|
+
USER_INJECTION = "user_injection"
|
|
28
|
+
# v1.7
|
|
29
|
+
TASK_CRITIQUED = "task_critiqued"
|
|
30
|
+
AGENT_BROADCAST = "agent_broadcast"
|
|
31
|
+
PREFETCH_HIT = "prefetch_hit"
|
|
32
|
+
PREFETCH_MISS = "prefetch_miss"
|
|
33
|
+
TASK_STRUCTURED_OUTPUT_CORRECTED = "task_structured_output_corrected"
|
|
34
|
+
# v1.8
|
|
35
|
+
PLANNER_KG_CONTEXT_INJECTED = "planner_kg_context_injected"
|
|
36
|
+
KNOWLEDGE_EXTRACTED = "knowledge_extracted"
|
|
37
|
+
MEMORY_CONSOLIDATED = "memory_consolidated"
|
|
38
|
+
# v2.0
|
|
39
|
+
PROVIDER_FALLBACK = "provider_fallback"
|
|
40
|
+
# v2.1
|
|
41
|
+
TASK_REJECTED_BY_HUMAN = "task_rejected_by_human"
|
|
42
|
+
|
|
43
|
+
class Event(BaseModel):
|
|
44
|
+
timestamp: datetime
|
|
45
|
+
type: events
|
|
46
|
+
payload: dict
|
|
47
|
+
|
|
48
|
+
@model_validator(mode="after")
|
|
49
|
+
def _payload_must_be_json_safe(self) -> "Event":
|
|
50
|
+
try:
|
|
51
|
+
json.dumps(self.payload)
|
|
52
|
+
except TypeError as e:
|
|
53
|
+
raise EventSerializationError(f"Event payload not JSON-safe: {e}") from e
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict:
|
|
57
|
+
ts = self.timestamp
|
|
58
|
+
if hasattr(ts, "isoformat"):
|
|
59
|
+
ts_str = ts.isoformat()
|
|
60
|
+
else:
|
|
61
|
+
ts_str = str(ts)
|
|
62
|
+
type_val = self.type.value if hasattr(self.type, "value") else str(self.type)
|
|
63
|
+
return {
|
|
64
|
+
"timestamp": ts_str,
|
|
65
|
+
"type": type_val,
|
|
66
|
+
"payload": self.payload,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_dict(cls, data: dict) -> "Event":
|
|
71
|
+
ts = data.get("timestamp", "")
|
|
72
|
+
if isinstance(ts, str):
|
|
73
|
+
try:
|
|
74
|
+
if ts.endswith("Z"):
|
|
75
|
+
ts = ts.replace("Z", "+00:00")
|
|
76
|
+
dt = datetime.fromisoformat(ts)
|
|
77
|
+
except ValueError:
|
|
78
|
+
dt = datetime.now(timezone.utc)
|
|
79
|
+
else:
|
|
80
|
+
dt = datetime.now(timezone.utc)
|
|
81
|
+
type_val = data.get("type", "swarm_started")
|
|
82
|
+
try:
|
|
83
|
+
event_type = events(type_val) if isinstance(type_val, str) else events.SWARM_STARTED
|
|
84
|
+
except ValueError:
|
|
85
|
+
event_type = events.SWARM_STARTED
|
|
86
|
+
return cls(
|
|
87
|
+
timestamp=dt,
|
|
88
|
+
type=event_type,
|
|
89
|
+
payload=dict(data.get("payload", {})),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def to_json(self) -> str:
|
|
93
|
+
return json.dumps(self.to_dict())
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_json(cls, raw: str) -> "Event":
|
|
97
|
+
return cls.from_dict(json.loads(raw))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Exceptions for devsper types and bus."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EventSerializationError(Exception):
|
|
5
|
+
"""Raised when an event payload is not JSON-serializable."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskNotFoundError(Exception):
|
|
9
|
+
"""Raised when a task ID is not found in the scheduler."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BusConnectionError(Exception):
|
|
13
|
+
"""Raised when the message bus backend cannot connect (e.g. Redis unreachable)."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CheckpointNotFoundError(Exception):
|
|
17
|
+
"""Raised when a checkpoint file for a run_id is not found."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MCPToolError(Exception):
|
|
21
|
+
"""Raised when an MCP tools/call returns an error or invalid response."""
|
devsper/types/swarm.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Literal, get_args
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
ModelName = Literal[
|
|
5
|
+
"claude-opus-4-6",
|
|
6
|
+
"claude-sonnet-4-6",
|
|
7
|
+
"claude-haiku-4-5-20251001",
|
|
8
|
+
"claude-opus-4-20250514",
|
|
9
|
+
"claude-sonnet-4-20250514",
|
|
10
|
+
"claude-sonnet-3-7-20250219",
|
|
11
|
+
"claude-haiku-3-5-20241022",
|
|
12
|
+
"gpt-5.4",
|
|
13
|
+
"gpt-5.4-pro",
|
|
14
|
+
"gpt-5.2",
|
|
15
|
+
"gpt-5.1",
|
|
16
|
+
"gpt-4.1",
|
|
17
|
+
"gpt-4.1-mini",
|
|
18
|
+
"gpt-4.1-nano",
|
|
19
|
+
"gpt-4o",
|
|
20
|
+
"o3",
|
|
21
|
+
"o3-pro",
|
|
22
|
+
"o4-mini",
|
|
23
|
+
"gemini-3.1-pro-preview",
|
|
24
|
+
"gemini-3.1-flash-lite-preview",
|
|
25
|
+
"gemini-3-flash",
|
|
26
|
+
"gemini-2.5-pro",
|
|
27
|
+
"gemini-2.5-flash",
|
|
28
|
+
"gemini-2.5-flash-lite",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
available_models: list[str] = list(get_args(ModelName))
|
|
32
|
+
|
|
33
|
+
DEFAULT_WORKER_MODEL: ModelName = "claude-haiku-4-5-20251001"
|
|
34
|
+
DEFAULT_PLANNER_MODEL: ModelName = "claude-opus-4-6"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Swarm(BaseModel):
|
|
38
|
+
worker_count: int
|
|
39
|
+
worker_model: ModelName = DEFAULT_WORKER_MODEL
|
|
40
|
+
planner_model: ModelName = DEFAULT_PLANNER_MODEL
|
|
41
|
+
models: list[str] = Field(default_factory=lambda: list(get_args(ModelName)))
|
devsper/types/task.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TaskStatus(Enum):
|
|
8
|
+
PENDING = 0
|
|
9
|
+
RUNNING = 1
|
|
10
|
+
COMPLETED = 2
|
|
11
|
+
FAILED = -1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Task(BaseModel):
|
|
15
|
+
id: str
|
|
16
|
+
description: str
|
|
17
|
+
dependencies: list[str] = []
|
|
18
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
19
|
+
result: str | None = None
|
|
20
|
+
error: str | None = None # v1.9: error message when failed
|
|
21
|
+
speculative: bool = False
|
|
22
|
+
role: str | None = None # Optional agent role: research, code, analysis, critic
|
|
23
|
+
retry_count: int = 0 # v1.7: critic retries
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> dict:
|
|
26
|
+
"""Return all fields as JSON-safe dict."""
|
|
27
|
+
return {
|
|
28
|
+
"id": self.id,
|
|
29
|
+
"description": self.description,
|
|
30
|
+
"dependencies": list(self.dependencies),
|
|
31
|
+
"status": self.status.value if hasattr(self.status, "value") else str(self.status),
|
|
32
|
+
"result": self.result,
|
|
33
|
+
"error": self.error,
|
|
34
|
+
"speculative": self.speculative,
|
|
35
|
+
"role": self.role,
|
|
36
|
+
"retry_count": self.retry_count,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_dict(cls, data: dict) -> "Task":
|
|
41
|
+
"""Reconstruct Task from dict. Parse status back to TaskStatus enum."""
|
|
42
|
+
status = data.get("status", TaskStatus.PENDING)
|
|
43
|
+
if isinstance(status, int):
|
|
44
|
+
task_status = TaskStatus(status)
|
|
45
|
+
elif isinstance(status, str):
|
|
46
|
+
name_to_status = {
|
|
47
|
+
"PENDING": TaskStatus.PENDING,
|
|
48
|
+
"RUNNING": TaskStatus.RUNNING,
|
|
49
|
+
"COMPLETED": TaskStatus.COMPLETED,
|
|
50
|
+
"FAILED": TaskStatus.FAILED,
|
|
51
|
+
"0": TaskStatus.PENDING,
|
|
52
|
+
"1": TaskStatus.RUNNING,
|
|
53
|
+
"2": TaskStatus.COMPLETED,
|
|
54
|
+
"-1": TaskStatus.FAILED,
|
|
55
|
+
}
|
|
56
|
+
task_status = name_to_status.get(status.upper(), TaskStatus.PENDING)
|
|
57
|
+
else:
|
|
58
|
+
task_status = TaskStatus.PENDING
|
|
59
|
+
return cls(
|
|
60
|
+
id=data["id"],
|
|
61
|
+
description=data.get("description", ""),
|
|
62
|
+
dependencies=list(data.get("dependencies", [])),
|
|
63
|
+
status=task_status,
|
|
64
|
+
result=data.get("result"),
|
|
65
|
+
error=data.get("error"),
|
|
66
|
+
speculative=data.get("speculative", False),
|
|
67
|
+
role=data.get("role"),
|
|
68
|
+
retry_count=data.get("retry_count", 0),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def to_json(self) -> str:
|
|
72
|
+
return json.dumps(self.to_dict())
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_json(cls, raw: str) -> "Task":
|
|
76
|
+
return cls.from_dict(json.loads(raw))
|
|
77
|
+
|
|
78
|
+
def checksum(self) -> str:
|
|
79
|
+
"""SHA256 of to_json(); used to detect state drift between nodes."""
|
|
80
|
+
return hashlib.sha256(self.to_json().encode("utf-8")).hexdigest()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Upgrade module: check for updates, changelog, installer detection, perform upgrade.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .version_check import (
|
|
6
|
+
is_update_available,
|
|
7
|
+
get_current_version,
|
|
8
|
+
get_latest_version,
|
|
9
|
+
)
|
|
10
|
+
from .installer import detect_installer, perform_install
|
|
11
|
+
from .changelog import fetch_changelog, get_changes_between
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"is_update_available",
|
|
15
|
+
"get_current_version",
|
|
16
|
+
"get_latest_version",
|
|
17
|
+
"detect_installer",
|
|
18
|
+
"perform_install",
|
|
19
|
+
"fetch_changelog",
|
|
20
|
+
"get_changes_between",
|
|
21
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Changelog fetching and parsing: raw CHANGELOG.md, version sections, Rich formatting.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .version_check import get_version_diff_type, parse_semver
|
|
11
|
+
|
|
12
|
+
# Adjust org/repo if needed (e.g. for forks)
|
|
13
|
+
CHANGELOG_URL = "https://raw.githubusercontent.com/anthropic/devsper/main/CHANGELOG.md"
|
|
14
|
+
MAX_LINES_PER_VERSION = 8
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fetch_changelog() -> str | None:
|
|
18
|
+
"""Fetch raw CHANGELOG.md content; return None on failure."""
|
|
19
|
+
try:
|
|
20
|
+
resp = httpx.get(CHANGELOG_URL, timeout=10.0)
|
|
21
|
+
resp.raise_for_status()
|
|
22
|
+
return resp.text
|
|
23
|
+
except httpx.HTTPError:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Match ## [1.2.3], ## [1.2.3] - date, ## 1.2.3, ## 1.2.3 - date
|
|
28
|
+
_VERSION_HEADER = re.compile(
|
|
29
|
+
r"^##\s+\[?(?P<version>\d+\.\d+\.\d+)\]?(?:\s*-\s*[^\n]*)?\s*$", re.MULTILINE
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_changelog(content: str) -> dict[str, str]:
|
|
34
|
+
"""
|
|
35
|
+
Parse markdown changelog into {"1.2.3": "release notes text", ...}.
|
|
36
|
+
Sections start with ## [1.2.3] or ## 1.2.3.
|
|
37
|
+
"""
|
|
38
|
+
out: dict[str, str] = {}
|
|
39
|
+
matches = list(_VERSION_HEADER.finditer(content))
|
|
40
|
+
for i, m in enumerate(matches):
|
|
41
|
+
version = m.group("version")
|
|
42
|
+
start = m.end()
|
|
43
|
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
|
|
44
|
+
block = content[start:end].strip()
|
|
45
|
+
# Normalize: drop leading/trailing blank lines, keep structure
|
|
46
|
+
out[version] = block
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _version_in_range(v: str, current: str, latest: str) -> bool:
|
|
51
|
+
"""True if v is strictly after current and <= latest (by semver)."""
|
|
52
|
+
cur = parse_semver(current)
|
|
53
|
+
lat = parse_semver(latest)
|
|
54
|
+
v_tup = parse_semver(v)
|
|
55
|
+
if v_tup <= cur:
|
|
56
|
+
return False
|
|
57
|
+
if v_tup > lat:
|
|
58
|
+
return False
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_changes_between(
|
|
63
|
+
changelog: dict[str, str], current: str, latest: str
|
|
64
|
+
) -> list[dict]:
|
|
65
|
+
"""
|
|
66
|
+
Return list of {"version": "1.2.3", "notes": "...", "type": "major|minor|patch"}
|
|
67
|
+
for all versions between current (exclusive) and latest (inclusive).
|
|
68
|
+
Type is the bump from the previous release to this one. Sorted newest-first.
|
|
69
|
+
"""
|
|
70
|
+
cur_tup = parse_semver(current)
|
|
71
|
+
lat_tup = parse_semver(latest)
|
|
72
|
+
if lat_tup <= cur_tup:
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
candidates = [
|
|
76
|
+
(ver, notes)
|
|
77
|
+
for ver, notes in changelog.items()
|
|
78
|
+
if _version_in_range(ver, current, latest)
|
|
79
|
+
]
|
|
80
|
+
# Sort oldest first so we can compute "previous" for each
|
|
81
|
+
candidates.sort(key=lambda x: parse_semver(x[0]))
|
|
82
|
+
result: list[dict] = []
|
|
83
|
+
prev = current
|
|
84
|
+
for ver, notes in candidates:
|
|
85
|
+
diff_type: Literal["major", "minor", "patch"] = get_version_diff_type(
|
|
86
|
+
prev, ver
|
|
87
|
+
)
|
|
88
|
+
result.append({"version": ver, "notes": notes, "type": diff_type})
|
|
89
|
+
prev = ver
|
|
90
|
+
result.reverse() # newest first
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def format_changelog_rich(changes: list[dict]) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Format for terminal with Rich markup.
|
|
97
|
+
Major: [bold red], minor: [bold yellow], patch: [bold green].
|
|
98
|
+
Max MAX_LINES_PER_VERSION lines per version, then "... and N more changes".
|
|
99
|
+
"""
|
|
100
|
+
lines: list[str] = []
|
|
101
|
+
for entry in changes:
|
|
102
|
+
ver = entry["version"]
|
|
103
|
+
notes = entry["notes"]
|
|
104
|
+
t = entry["type"]
|
|
105
|
+
if t == "major":
|
|
106
|
+
header = f"[bold red]Version {ver}[/bold red]"
|
|
107
|
+
elif t == "minor":
|
|
108
|
+
header = f"[bold yellow]Version {ver}[/bold yellow]"
|
|
109
|
+
else:
|
|
110
|
+
header = f"[bold green]Version {ver}[/bold green]"
|
|
111
|
+
lines.append(header)
|
|
112
|
+
note_lines = [ln.strip() for ln in notes.splitlines() if ln.strip()]
|
|
113
|
+
# Skip sub-headers like "### Added" when counting bullets; treat as structure
|
|
114
|
+
bullet_lines = [ln for ln in note_lines if re.match(r"^[-*]|\d+\.", ln)]
|
|
115
|
+
if not bullet_lines:
|
|
116
|
+
bullet_lines = note_lines[:MAX_LINES_PER_VERSION]
|
|
117
|
+
shown = bullet_lines[:MAX_LINES_PER_VERSION]
|
|
118
|
+
for ln in shown:
|
|
119
|
+
lines.append(f" • {ln}" if not ln.startswith("•") else f" {ln}")
|
|
120
|
+
remaining = len(bullet_lines) - len(shown)
|
|
121
|
+
if remaining > 0:
|
|
122
|
+
lines.append(f" [dim]... and {remaining} more changes[/dim]")
|
|
123
|
+
lines.append("")
|
|
124
|
+
return "\n".join(lines).strip()
|
devsper/upgrade/cli.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devsper upgrade subcommand: version check, changelog, installer detection, install, verify.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.live import Live
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.prompt import Confirm
|
|
12
|
+
from rich.spinner import Spinner
|
|
13
|
+
from .version_check import (
|
|
14
|
+
get_current_version,
|
|
15
|
+
get_latest_version,
|
|
16
|
+
is_update_available,
|
|
17
|
+
get_version_diff_type,
|
|
18
|
+
)
|
|
19
|
+
from .changelog import fetch_changelog, parse_changelog, get_changes_between, format_changelog_rich
|
|
20
|
+
from .installer import detect_installer, get_install_command, perform_install, verify_installation
|
|
21
|
+
from .notifier import suppress_notifications
|
|
22
|
+
|
|
23
|
+
_console = Console(stderr=True)
|
|
24
|
+
PACKAGE = "devsper"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _diff_type_color(t: Literal["major", "minor", "patch"]) -> str:
|
|
28
|
+
return {"major": "red", "minor": "yellow", "patch": "green"}.get(t, "white")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_upgrade(args: object) -> int:
|
|
32
|
+
"""
|
|
33
|
+
Entry point for 'devsper upgrade'.
|
|
34
|
+
Supports: --check, --yes/-y, --version VERSION, --dry-run.
|
|
35
|
+
"""
|
|
36
|
+
# Avoid startup nag during upgrade
|
|
37
|
+
suppress_notifications()
|
|
38
|
+
|
|
39
|
+
check_only = getattr(args, "check", False)
|
|
40
|
+
yes = getattr(args, "yes", False)
|
|
41
|
+
version_override = getattr(args, "version", None)
|
|
42
|
+
dry_run = getattr(args, "dry_run", False)
|
|
43
|
+
|
|
44
|
+
current = get_current_version()
|
|
45
|
+
|
|
46
|
+
# ---- Step 1: Version check ----
|
|
47
|
+
if version_override:
|
|
48
|
+
latest = version_override
|
|
49
|
+
from .version_check import parse_semver
|
|
50
|
+
cur_t = parse_semver(current)
|
|
51
|
+
lat_t = parse_semver(latest)
|
|
52
|
+
available = lat_t > cur_t
|
|
53
|
+
_console.print(f"[dim]Installing specified version {latest}[/dim]")
|
|
54
|
+
else:
|
|
55
|
+
_console.print("[dim]Fetching latest version from PyPI...[/dim]")
|
|
56
|
+
try:
|
|
57
|
+
available, _, latest = is_update_available()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
_console.print(f"[red]Error checking PyPI: {e}[/red]")
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
diff_type = get_version_diff_type(current, latest)
|
|
63
|
+
color = _diff_type_color(diff_type)
|
|
64
|
+
_console.print(f"{PACKAGE} [bold]{current}[/bold] → [bold]{latest}[/bold] [{color}][{diff_type} update][/{color}]")
|
|
65
|
+
|
|
66
|
+
if check_only:
|
|
67
|
+
if not available and not version_override:
|
|
68
|
+
_console.print("[green]You are on the latest version.[/green]")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
if not available and not version_override:
|
|
72
|
+
_console.print("[green]Already on latest version. Use --version X.Y.Z to install a specific version.[/green]")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
# Specific version check: ensure it exists on PyPI (optional: could add a quick PyPI check)
|
|
76
|
+
if version_override:
|
|
77
|
+
# We'll discover "not found" during install
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# ---- Step 2: Changelog ----
|
|
81
|
+
changelog_raw = fetch_changelog()
|
|
82
|
+
changes: list[dict] = []
|
|
83
|
+
if changelog_raw:
|
|
84
|
+
parsed = parse_changelog(changelog_raw)
|
|
85
|
+
changes = get_changes_between(parsed, current, latest)
|
|
86
|
+
if changes:
|
|
87
|
+
if len(changes) > 3:
|
|
88
|
+
_console.print(f"[dim]Showing changes across {len(changes)} releases[/dim]")
|
|
89
|
+
formatted = format_changelog_rich(changes)
|
|
90
|
+
_console.print(Panel(formatted, title="Changelog", border_style="blue"))
|
|
91
|
+
else:
|
|
92
|
+
_console.print("[dim]Changelog unavailable (network or repo).[/dim]")
|
|
93
|
+
|
|
94
|
+
# ---- Step 3: Installer detection ----
|
|
95
|
+
installer = detect_installer()
|
|
96
|
+
if installer == "uv":
|
|
97
|
+
_console.print(" [green]Installing with uv[/green] ✓")
|
|
98
|
+
_console.print(" [dim]uv detected in environment[/dim]")
|
|
99
|
+
else:
|
|
100
|
+
_console.print(" Installing with [yellow]pip[/yellow]")
|
|
101
|
+
|
|
102
|
+
if dry_run:
|
|
103
|
+
cmd = get_install_command(installer, version=latest)
|
|
104
|
+
_console.print(f"[dim]Dry run: would run: {' '.join(cmd)}[/dim]")
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
# ---- Step 4: Confirmation ----
|
|
108
|
+
if not yes:
|
|
109
|
+
proceed = Confirm.ask("Proceed with upgrade?", default=True, console=_console)
|
|
110
|
+
if not proceed:
|
|
111
|
+
_console.print("Cancelled.")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
# ---- Step 5: Installation with progress ----
|
|
115
|
+
with Live(
|
|
116
|
+
Spinner("dots", text=f" Installing {PACKAGE} {latest}..."),
|
|
117
|
+
console=_console,
|
|
118
|
+
refresh_per_second=10,
|
|
119
|
+
):
|
|
120
|
+
success, output = perform_install(installer, version=latest)
|
|
121
|
+
|
|
122
|
+
if not success:
|
|
123
|
+
_console.print("[red]Installation failed:[/red]")
|
|
124
|
+
_console.print(output)
|
|
125
|
+
_console.print("[dim]Try running with --verbose or use pip/uv directly.[/dim]")
|
|
126
|
+
if "Permission denied" in output or "permission" in output.lower():
|
|
127
|
+
_console.print("[yellow]Tip: use a virtualenv or run with appropriate permissions.[/yellow]")
|
|
128
|
+
if "404" in output or "not found" in output.lower():
|
|
129
|
+
_console.print(f"[yellow]Version {latest} may not exist on PyPI.[/yellow]")
|
|
130
|
+
return 1
|
|
131
|
+
|
|
132
|
+
# ---- Step 6: Verification ----
|
|
133
|
+
if verify_installation(latest):
|
|
134
|
+
_console.print(f" [green]✓ Successfully upgraded to {PACKAGE} {latest}[/green]")
|
|
135
|
+
else:
|
|
136
|
+
_console.print(f" [yellow]Upgrade completed but version check failed. Run [bold]devsper --version[/bold] to confirm.[/yellow]")
|
|
137
|
+
|
|
138
|
+
# ---- Step 7: Post-install summary ----
|
|
139
|
+
if diff_type == "major":
|
|
140
|
+
_console.print(" [bold red]⚠ Major update — review breaking changes above[/bold red]")
|
|
141
|
+
_console.print(" [dim]Run devsper --help to see commands[/dim]")
|
|
142
|
+
if changes:
|
|
143
|
+
_console.print(f" [dim]{len(changes)} release(s) applied.[/dim]")
|
|
144
|
+
|
|
145
|
+
return 0
|