runsight 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runsight-0.1.0/.gitignore +2 -0
- runsight-0.1.0/PKG-INFO +20 -0
- runsight-0.1.0/pyproject.toml +53 -0
- runsight-0.1.0/src/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/alembic/env.py +65 -0
- runsight-0.1.0/src/runsight_api/alembic/script.py.mako +27 -0
- runsight-0.1.0/src/runsight_api/alembic/versions/001_initial_schema.py +29 -0
- runsight-0.1.0/src/runsight_api/alembic.ini +36 -0
- runsight-0.1.0/src/runsight_api/cli.py +39 -0
- runsight-0.1.0/src/runsight_api/core/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/core/config.py +76 -0
- runsight-0.1.0/src/runsight_api/core/context.py +47 -0
- runsight-0.1.0/src/runsight_api/core/di.py +25 -0
- runsight-0.1.0/src/runsight_api/core/logging.py +62 -0
- runsight-0.1.0/src/runsight_api/core/project.py +167 -0
- runsight-0.1.0/src/runsight_api/core/secrets.py +102 -0
- runsight-0.1.0/src/runsight_api/data/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/data/filesystem/__init__.py +13 -0
- runsight-0.1.0/src/runsight_api/data/filesystem/_base_yaml_repo.py +118 -0
- runsight-0.1.0/src/runsight_api/data/filesystem/_utils.py +26 -0
- runsight-0.1.0/src/runsight_api/data/filesystem/provider_repo.py +180 -0
- runsight-0.1.0/src/runsight_api/data/filesystem/settings_repo.py +149 -0
- runsight-0.1.0/src/runsight_api/data/filesystem/soul_repo.py +17 -0
- runsight-0.1.0/src/runsight_api/data/filesystem/step_repo.py +10 -0
- runsight-0.1.0/src/runsight_api/data/filesystem/task_repo.py +10 -0
- runsight-0.1.0/src/runsight_api/data/filesystem/workflow_repo.py +617 -0
- runsight-0.1.0/src/runsight_api/data/repositories/__init__.py +3 -0
- runsight-0.1.0/src/runsight_api/data/repositories/run_repo.py +314 -0
- runsight-0.1.0/src/runsight_api/domain/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/domain/entities/__init__.py +13 -0
- runsight-0.1.0/src/runsight_api/domain/entities/log.py +14 -0
- runsight-0.1.0/src/runsight_api/domain/entities/run.py +129 -0
- runsight-0.1.0/src/runsight_api/domain/entities/settings.py +21 -0
- runsight-0.1.0/src/runsight_api/domain/errors.py +166 -0
- runsight-0.1.0/src/runsight_api/domain/events.py +64 -0
- runsight-0.1.0/src/runsight_api/domain/value_objects.py +78 -0
- runsight-0.1.0/src/runsight_api/logic/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/logic/observers/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/logic/observers/artifact_cleanup_observer.py +92 -0
- runsight-0.1.0/src/runsight_api/logic/observers/eval_observer.py +332 -0
- runsight-0.1.0/src/runsight_api/logic/observers/execution_observer.py +472 -0
- runsight-0.1.0/src/runsight_api/logic/observers/streaming_observer.py +161 -0
- runsight-0.1.0/src/runsight_api/logic/services/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/logic/services/eval_service.py +385 -0
- runsight-0.1.0/src/runsight_api/logic/services/execution_service.py +527 -0
- runsight-0.1.0/src/runsight_api/logic/services/git_service.py +133 -0
- runsight-0.1.0/src/runsight_api/logic/services/model_service.py +77 -0
- runsight-0.1.0/src/runsight_api/logic/services/provider_service.py +278 -0
- runsight-0.1.0/src/runsight_api/logic/services/registry_service.py +52 -0
- runsight-0.1.0/src/runsight_api/logic/services/run_service.py +125 -0
- runsight-0.1.0/src/runsight_api/logic/services/settings_service.py +165 -0
- runsight-0.1.0/src/runsight_api/logic/services/soul_service.py +235 -0
- runsight-0.1.0/src/runsight_api/logic/services/workflow_service.py +176 -0
- runsight-0.1.0/src/runsight_api/main.py +212 -0
- runsight-0.1.0/src/runsight_api/transport/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/transport/deps.py +141 -0
- runsight-0.1.0/src/runsight_api/transport/middleware/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/transport/middleware/access_log.py +28 -0
- runsight-0.1.0/src/runsight_api/transport/middleware/error_handler.py +29 -0
- runsight-0.1.0/src/runsight_api/transport/middleware/request_id.py +20 -0
- runsight-0.1.0/src/runsight_api/transport/routers/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/transport/routers/dashboard.py +117 -0
- runsight-0.1.0/src/runsight_api/transport/routers/eval.py +21 -0
- runsight-0.1.0/src/runsight_api/transport/routers/git.py +299 -0
- runsight-0.1.0/src/runsight_api/transport/routers/models.py +88 -0
- runsight-0.1.0/src/runsight_api/transport/routers/runs.py +374 -0
- runsight-0.1.0/src/runsight_api/transport/routers/settings.py +261 -0
- runsight-0.1.0/src/runsight_api/transport/routers/souls.py +71 -0
- runsight-0.1.0/src/runsight_api/transport/routers/sse_stream.py +61 -0
- runsight-0.1.0/src/runsight_api/transport/routers/steps.py +126 -0
- runsight-0.1.0/src/runsight_api/transport/routers/tasks.py +126 -0
- runsight-0.1.0/src/runsight_api/transport/routers/tools.py +53 -0
- runsight-0.1.0/src/runsight_api/transport/routers/workflows.py +125 -0
- runsight-0.1.0/src/runsight_api/transport/schemas/__init__.py +0 -0
- runsight-0.1.0/src/runsight_api/transport/schemas/dashboard.py +28 -0
- runsight-0.1.0/src/runsight_api/transport/schemas/eval.py +43 -0
- runsight-0.1.0/src/runsight_api/transport/schemas/runs.py +89 -0
- runsight-0.1.0/src/runsight_api/transport/schemas/settings.py +22 -0
- runsight-0.1.0/src/runsight_api/transport/schemas/souls.py +64 -0
- runsight-0.1.0/src/runsight_api/transport/schemas/steps.py +29 -0
- runsight-0.1.0/src/runsight_api/transport/schemas/tasks.py +29 -0
- runsight-0.1.0/src/runsight_api/transport/schemas/tools.py +11 -0
- runsight-0.1.0/src/runsight_api/transport/schemas/workflows.py +92 -0
- runsight-0.1.0/tests/__init__.py +0 -0
- runsight-0.1.0/tests/conftest.py +20 -0
- runsight-0.1.0/tests/core/__init__.py +0 -0
- runsight-0.1.0/tests/core/test_config.py +23 -0
- runsight-0.1.0/tests/core/test_project.py +132 -0
- runsight-0.1.0/tests/core/test_run373_git_init.py +180 -0
- runsight-0.1.0/tests/core/test_scaffold_project.py +193 -0
- runsight-0.1.0/tests/data/test_base_yaml_repository.py +237 -0
- runsight-0.1.0/tests/data/test_baseline_query.py +308 -0
- runsight-0.1.0/tests/data/test_filesystem.py +258 -0
- runsight-0.1.0/tests/data/test_path_traversal.py +324 -0
- runsight-0.1.0/tests/data/test_repositories.py +30 -0
- runsight-0.1.0/tests/data/test_run478_run_repo_workflow_health_metrics.py +110 -0
- runsight-0.1.0/tests/data/test_run479_run_repo_list_metrics.py +207 -0
- runsight-0.1.0/tests/data/test_run480_run_repo_delete_cascade.py +239 -0
- runsight-0.1.0/tests/data/test_run603_workflow_interface_validation.py +215 -0
- runsight-0.1.0/tests/data/test_run606_snapshot_validation.py +709 -0
- runsight-0.1.0/tests/data/test_soul_repo.py +99 -0
- runsight-0.1.0/tests/data/test_step_repo.py +33 -0
- runsight-0.1.0/tests/data/test_task_repo.py +33 -0
- runsight-0.1.0/tests/domain/test_entities.py +20 -0
- runsight-0.1.0/tests/domain/test_entity_extra_config.py +111 -0
- runsight-0.1.0/tests/domain/test_node_status_enum.py +267 -0
- runsight-0.1.0/tests/domain/test_run127_bug_fixes.py +151 -0
- runsight-0.1.0/tests/domain/test_run127_create_run_workflow_name.py +52 -0
- runsight-0.1.0/tests/domain/test_run329_workflow_commit_sha.py +319 -0
- runsight-0.1.0/tests/domain/test_run334_state_transition_guards.py +471 -0
- runsight-0.1.0/tests/domain/test_run379_data_model.py +337 -0
- runsight-0.1.0/tests/domain/test_run437_soul_schema_alignment.py +94 -0
- runsight-0.1.0/tests/domain/test_run472_transport_soul_schema.py +82 -0
- runsight-0.1.0/tests/domain/test_run556_workflow_entity_enabled.py +69 -0
- runsight-0.1.0/tests/domain/test_run663_entity_fields.py +105 -0
- runsight-0.1.0/tests/domain/test_run689_soul_assertions_shim.py +38 -0
- runsight-0.1.0/tests/domain/test_run_node_eval_fields.py +225 -0
- runsight-0.1.0/tests/domain/test_sse_event_constants.py +301 -0
- runsight-0.1.0/tests/logic/test_artifact_cleanup_fire_and_forget.py +69 -0
- runsight-0.1.0/tests/logic/test_artifact_cleanup_observer.py +301 -0
- runsight-0.1.0/tests/logic/test_artifact_store_injection.py +138 -0
- runsight-0.1.0/tests/logic/test_budget_run_status.py +518 -0
- runsight-0.1.0/tests/logic/test_disabled_provider_policy.py +155 -0
- runsight-0.1.0/tests/logic/test_eval_observer.py +911 -0
- runsight-0.1.0/tests/logic/test_execution_observer.py +706 -0
- runsight-0.1.0/tests/logic/test_execution_observer_soul.py +225 -0
- runsight-0.1.0/tests/logic/test_execution_service.py +865 -0
- runsight-0.1.0/tests/logic/test_execution_service_concurrency.py +616 -0
- runsight-0.1.0/tests/logic/test_execution_service_main_branch_loading.py +105 -0
- runsight-0.1.0/tests/logic/test_google_api_key_header.py +1 -0
- runsight-0.1.0/tests/logic/test_provider_pulse_release_digest_eval_configs.py +46 -0
- runsight-0.1.0/tests/logic/test_provider_service.py +447 -0
- runsight-0.1.0/tests/logic/test_registry_service.py +132 -0
- runsight-0.1.0/tests/logic/test_run135_sim_branches.py +446 -0
- runsight-0.1.0/tests/logic/test_run141_execution_service_api_keys.py +153 -0
- runsight-0.1.0/tests/logic/test_run180_observer_block_result.py +345 -0
- runsight-0.1.0/tests/logic/test_run200_state_flow.py +270 -0
- runsight-0.1.0/tests/logic/test_run207_streaming_observer.py +563 -0
- runsight-0.1.0/tests/logic/test_run208_double_observer.py +446 -0
- runsight-0.1.0/tests/logic/test_run209_cancel_wiring.py +280 -0
- runsight-0.1.0/tests/logic/test_run347_wire_assertion_configs.py +304 -0
- runsight-0.1.0/tests/logic/test_run374_git_service.py +371 -0
- runsight-0.1.0/tests/logic/test_run376_save_commit.py +446 -0
- runsight-0.1.0/tests/logic/test_run380_branch_aware_exec.py +477 -0
- runsight-0.1.0/tests/logic/test_run404_workflow_id_filter.py +135 -0
- runsight-0.1.0/tests/logic/test_run408_async_httpx.py +588 -0
- runsight-0.1.0/tests/logic/test_run470_soul_update_preservation.py +104 -0
- runsight-0.1.0/tests/logic/test_run474_soul_service_workflow_repo_ownership.py +102 -0
- runsight-0.1.0/tests/logic/test_run478_workflow_health_metrics.py +174 -0
- runsight-0.1.0/tests/logic/test_run558_regression_logic.py +569 -0
- runsight-0.1.0/tests/logic/test_run573_library_soul_usage_scanning.py +693 -0
- runsight-0.1.0/tests/logic/test_run606_execution_service_snapshot_resolution.py +578 -0
- runsight-0.1.0/tests/logic/test_run607_nested_run_lifecycle.py +549 -0
- runsight-0.1.0/tests/logic/test_run699_eval_observer_transform.py +350 -0
- runsight-0.1.0/tests/logic/test_run_service.py +319 -0
- runsight-0.1.0/tests/logic/test_single_writer_and_session.py +129 -0
- runsight-0.1.0/tests/logic/test_soul_service.py +564 -0
- runsight-0.1.0/tests/logic/test_ssrf_provider.py +416 -0
- runsight-0.1.0/tests/logic/test_workflow_service.py +641 -0
- runsight-0.1.0/tests/test_blank_canvas_yaml_bug.py +202 -0
- runsight-0.1.0/tests/test_cors_config.py +137 -0
- runsight-0.1.0/tests/test_execution_log_persistence.py +409 -0
- runsight-0.1.0/tests/test_execution_service_return_type.py +49 -0
- runsight-0.1.0/tests/test_main.py +11 -0
- runsight-0.1.0/tests/test_request_id_access_log.py +233 -0
- runsight-0.1.0/tests/test_run134_openapi_codegen.py +240 -0
- runsight-0.1.0/tests/test_run203_placeholder_migration_cleanup.py +294 -0
- runsight-0.1.0/tests/test_run378_source_branch_filtering.py +643 -0
- runsight-0.1.0/tests/test_run516_ci_workspace_layout.py +217 -0
- runsight-0.1.0/tests/test_run517_codebones_index_regression.py +151 -0
- runsight-0.1.0/tests/test_run519_tracked_artifact_cleanup.py +98 -0
- runsight-0.1.0/tests/test_run520_tools_canonical_home.py +65 -0
- runsight-0.1.0/tests/test_run691_stale_soul_assertion_refs.py +112 -0
- runsight-0.1.0/tests/test_run705_assertions_fire_e2e.py +716 -0
- runsight-0.1.0/tests/test_run706_api_e2e.py +539 -0
- runsight-0.1.0/tests/test_run707_sse_streaming_e2e.py +902 -0
- runsight-0.1.0/tests/test_run739_source_audit.py +286 -0
- runsight-0.1.0/tests/test_run740_source_audit.py +225 -0
- runsight-0.1.0/tests/test_sql_pagination.py +132 -0
- runsight-0.1.0/tests/test_stale_run_recovery.py +233 -0
- runsight-0.1.0/tests/test_structlog_foundation.py +319 -0
- runsight-0.1.0/tests/test_structured_errors.py +385 -0
- runsight-0.1.0/tests/transport/test_dashboard_router.py +25 -0
- runsight-0.1.0/tests/transport/test_disabled_provider_souls_router.py +112 -0
- runsight-0.1.0/tests/transport/test_error_sanitization.py +173 -0
- runsight-0.1.0/tests/transport/test_eval_endpoints.py +615 -0
- runsight-0.1.0/tests/transport/test_git_router.py +272 -0
- runsight-0.1.0/tests/transport/test_git_security.py +442 -0
- runsight-0.1.0/tests/transport/test_model_catalog_api.py +598 -0
- runsight-0.1.0/tests/transport/test_run127_router_wiring.py +98 -0
- runsight-0.1.0/tests/transport/test_run144_polling_and_node_summary.py +355 -0
- runsight-0.1.0/tests/transport/test_run331_error_normalization.py +398 -0
- runsight-0.1.0/tests/transport/test_run338_dashboard_kpis.py +310 -0
- runsight-0.1.0/tests/transport/test_run341_attention.py +557 -0
- runsight-0.1.0/tests/transport/test_run342_eval_kpis.py +499 -0
- runsight-0.1.0/tests/transport/test_run363_workflow_filter.py +230 -0
- runsight-0.1.0/tests/transport/test_run413_response_fields.py +136 -0
- runsight-0.1.0/tests/transport/test_run423_sim_branch_endpoint.py +237 -0
- runsight-0.1.0/tests/transport/test_run478_workflow_health_router.py +245 -0
- runsight-0.1.0/tests/transport/test_run479_runs_list_contract.py +122 -0
- runsight-0.1.0/tests/transport/test_run537_canonical_runs_route.py +137 -0
- runsight-0.1.0/tests/transport/test_run555_node_response_enrichment.py +403 -0
- runsight-0.1.0/tests/transport/test_run556_patch_workflow_enabled.py +426 -0
- runsight-0.1.0/tests/transport/test_run557_git_file_read.py +429 -0
- runsight-0.1.0/tests/transport/test_run558_regression_endpoints.py +442 -0
- runsight-0.1.0/tests/transport/test_run608_sse_child_lifecycle.py +324 -0
- runsight-0.1.0/tests/transport/test_run612_child_run_queries.py +386 -0
- runsight-0.1.0/tests/transport/test_runs_router.py +377 -0
- runsight-0.1.0/tests/transport/test_settings_router.py +316 -0
- runsight-0.1.0/tests/transport/test_souls_router.py +241 -0
- runsight-0.1.0/tests/transport/test_sse_stream.py +531 -0
- runsight-0.1.0/tests/transport/test_steps_router.py +94 -0
- runsight-0.1.0/tests/transport/test_tasks_router.py +94 -0
- runsight-0.1.0/tests/transport/test_tools_router.py +173 -0
- runsight-0.1.0/tests/transport/test_workflows_router.py +485 -0
- runsight-0.1.0/tests/unit/__init__.py +0 -0
- runsight-0.1.0/tests/unit/cleanup/__init__.py +0 -0
- runsight-0.1.0/tests/unit/cleanup/test_decrypt_stub_cleanup.py +66 -0
- runsight-0.1.0/tests/unit/cleanup/test_remove_sqlite_encryption.py +338 -0
- runsight-0.1.0/tests/unit/core/__init__.py +0 -0
- runsight-0.1.0/tests/unit/core/test_secrets.py +428 -0
- runsight-0.1.0/tests/unit/data/__init__.py +0 -0
- runsight-0.1.0/tests/unit/data/filesystem/__init__.py +0 -0
- runsight-0.1.0/tests/unit/data/filesystem/test_provider_repo.py +518 -0
- runsight-0.1.0/tests/unit/data/filesystem/test_run490_workflow_repo_tool_governance.py +185 -0
- runsight-0.1.0/tests/unit/data/filesystem/test_settings_repo.py +181 -0
- runsight-0.1.0/tests/unit/logic/__init__.py +0 -0
- runsight-0.1.0/tests/unit/logic/test_provider_service_wiring.py +566 -0
- runsight-0.1.0/tests/unit/logic/test_settings_service.py +452 -0
runsight-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runsight
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: YAML-first workflow engine for AI agents
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: alembic>=1.14.0
|
|
7
|
+
Requires-Dist: fastapi>=0.115.0
|
|
8
|
+
Requires-Dist: httpx>=0.28.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Requires-Dist: ruamel-yaml>=0.18.0
|
|
12
|
+
Requires-Dist: runsight-core
|
|
13
|
+
Requires-Dist: sqlmodel>=0.0.24
|
|
14
|
+
Requires-Dist: structlog>=24.0.0
|
|
15
|
+
Requires-Dist: uvicorn[standard]>=0.34.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: httpx>=0.28.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "runsight"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "YAML-first workflow engine for AI agents"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"fastapi>=0.115.0",
|
|
8
|
+
"uvicorn[standard]>=0.34.0",
|
|
9
|
+
"sqlmodel>=0.0.24",
|
|
10
|
+
"alembic>=1.14.0",
|
|
11
|
+
"pyyaml>=6.0",
|
|
12
|
+
"ruamel.yaml>=0.18.0",
|
|
13
|
+
"runsight-core",
|
|
14
|
+
"pydantic-settings>=2.0",
|
|
15
|
+
"httpx>=0.28.0",
|
|
16
|
+
"structlog>=24.0.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
runsight = "runsight_api.cli:main"
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8.0",
|
|
25
|
+
"pytest-asyncio>=0.24.0",
|
|
26
|
+
"httpx>=0.28.0",
|
|
27
|
+
"ruff>=0.8.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/runsight_api"]
|
|
36
|
+
artifacts = ["src/runsight_api/static/**"]
|
|
37
|
+
exclude = ["**/*.db"]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 100
|
|
41
|
+
target-version = "py311"
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint.per-file-ignores]
|
|
44
|
+
"tests/test_run134_openapi_codegen.py" = ["F401"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff.format]
|
|
47
|
+
quote-style = "double"
|
|
48
|
+
exclude = ["tests/test_run134_openapi_codegen.py"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
pythonpath = ["src"]
|
|
53
|
+
asyncio_mode = "auto"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from logging.config import fileConfig
|
|
2
|
+
|
|
3
|
+
from alembic import context
|
|
4
|
+
from sqlalchemy import engine_from_config, pool
|
|
5
|
+
from sqlmodel import SQLModel
|
|
6
|
+
|
|
7
|
+
from runsight_api.domain.entities.log import LogEntry # noqa: F401
|
|
8
|
+
|
|
9
|
+
# Import all models so SQLModel.metadata is populated
|
|
10
|
+
from runsight_api.domain.entities.run import Run, RunNode # noqa: F401
|
|
11
|
+
|
|
12
|
+
config = context.config
|
|
13
|
+
if config.config_file_name is not None:
|
|
14
|
+
fileConfig(config.config_file_name)
|
|
15
|
+
|
|
16
|
+
target_metadata = SQLModel.metadata
|
|
17
|
+
|
|
18
|
+
# Naming convention for constraints (helps with batch mode migrations)
|
|
19
|
+
NAMING_CONVENTION = {
|
|
20
|
+
"ix": "ix_%(column_0_label)s",
|
|
21
|
+
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
|
22
|
+
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
|
23
|
+
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
|
24
|
+
"pk": "pk_%(table_name)s",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_migrations_offline() -> None:
|
|
29
|
+
"""Run migrations in 'offline' mode."""
|
|
30
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
31
|
+
context.configure(
|
|
32
|
+
url=url,
|
|
33
|
+
target_metadata=target_metadata,
|
|
34
|
+
literal_binds=True,
|
|
35
|
+
dialect_opts={"paramstyle": "named"},
|
|
36
|
+
render_as_batch=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
with context.begin_transaction():
|
|
40
|
+
context.run_migrations()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_migrations_online() -> None:
|
|
44
|
+
"""Run migrations in 'online' mode."""
|
|
45
|
+
connectable = engine_from_config(
|
|
46
|
+
config.get_section(config.config_ini_section, {}),
|
|
47
|
+
prefix="sqlalchemy.",
|
|
48
|
+
poolclass=pool.NullPool,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
with connectable.connect() as connection:
|
|
52
|
+
context.configure(
|
|
53
|
+
connection=connection,
|
|
54
|
+
target_metadata=target_metadata,
|
|
55
|
+
render_as_batch=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
with context.begin_transaction():
|
|
59
|
+
context.run_migrations()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if context.is_offline_mode():
|
|
63
|
+
run_migrations_offline()
|
|
64
|
+
else:
|
|
65
|
+
run_migrations_online()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
import sqlmodel
|
|
13
|
+
${imports if imports else ""}
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = ${repr(up_revision)}
|
|
17
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
${upgrades if upgrades else "pass"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Initial schema — baseline migration.
|
|
2
|
+
|
|
3
|
+
Revision ID: 001_initial
|
|
4
|
+
Revises: None
|
|
5
|
+
Create Date: 2026-03-22
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
import sqlmodel
|
|
11
|
+
from alembic import op
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision: str = "001_initial"
|
|
15
|
+
down_revision: Union[str, None] = None
|
|
16
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
17
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade() -> None:
|
|
21
|
+
"""Create all tables from SQLModel metadata."""
|
|
22
|
+
bind = op.get_bind()
|
|
23
|
+
sqlmodel.SQLModel.metadata.create_all(bind)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
"""Drop all tables from SQLModel metadata."""
|
|
28
|
+
bind = op.get_bind()
|
|
29
|
+
sqlmodel.SQLModel.metadata.drop_all(bind)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[alembic]
|
|
2
|
+
script_location = alembic
|
|
3
|
+
sqlalchemy.url = sqlite:///./runsight.db
|
|
4
|
+
|
|
5
|
+
[loggers]
|
|
6
|
+
keys = root,sqlalchemy,alembic
|
|
7
|
+
|
|
8
|
+
[handlers]
|
|
9
|
+
keys = console
|
|
10
|
+
|
|
11
|
+
[formatters]
|
|
12
|
+
keys = generic
|
|
13
|
+
|
|
14
|
+
[logger_root]
|
|
15
|
+
level = WARN
|
|
16
|
+
handlers = console
|
|
17
|
+
|
|
18
|
+
[logger_sqlalchemy]
|
|
19
|
+
level = WARN
|
|
20
|
+
handlers =
|
|
21
|
+
qualname = sqlalchemy.engine
|
|
22
|
+
|
|
23
|
+
[logger_alembic]
|
|
24
|
+
level = INFO
|
|
25
|
+
handlers =
|
|
26
|
+
qualname = alembic
|
|
27
|
+
|
|
28
|
+
[handler_console]
|
|
29
|
+
class = StreamHandler
|
|
30
|
+
args = (sys.stderr,)
|
|
31
|
+
level = NOTSET
|
|
32
|
+
formatter = generic
|
|
33
|
+
|
|
34
|
+
[formatter_generic]
|
|
35
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
36
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""CLI entry point for `uvx runsight` / `runsight` command."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import uvicorn
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
# Parse --host and --port from argv (keep it minimal)
|
|
10
|
+
host = "0.0.0.0"
|
|
11
|
+
port = 8000
|
|
12
|
+
|
|
13
|
+
args = sys.argv[1:]
|
|
14
|
+
i = 0
|
|
15
|
+
while i < len(args):
|
|
16
|
+
if args[i] == "--port" and i + 1 < len(args):
|
|
17
|
+
port = int(args[i + 1])
|
|
18
|
+
i += 2
|
|
19
|
+
elif args[i] == "--host" and i + 1 < len(args):
|
|
20
|
+
host = args[i + 1]
|
|
21
|
+
i += 2
|
|
22
|
+
elif args[i] in ("--help", "-h"):
|
|
23
|
+
print("Usage: runsight [--host HOST] [--port PORT]")
|
|
24
|
+
print()
|
|
25
|
+
print("Start the Runsight server.")
|
|
26
|
+
print()
|
|
27
|
+
print("Options:")
|
|
28
|
+
print(" --host HOST Bind address (default: 0.0.0.0)")
|
|
29
|
+
print(" --port PORT Bind port (default: 8000)")
|
|
30
|
+
sys.exit(0)
|
|
31
|
+
else:
|
|
32
|
+
print(f"Unknown argument: {args[i]}", file=sys.stderr)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
print(f" Runsight running at http://localhost:{port}")
|
|
36
|
+
print(" Press Ctrl+C to stop")
|
|
37
|
+
print()
|
|
38
|
+
|
|
39
|
+
uvicorn.run("runsight_api.main:app", host=host, port=port)
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, model_validator
|
|
6
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
|
+
|
|
8
|
+
from .project import resolve_base_path
|
|
9
|
+
|
|
10
|
+
_DB_URL_SENTINEL = "__auto__"
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _default_base_path() -> str:
|
|
16
|
+
"""Compute the default base_path using project detection.
|
|
17
|
+
|
|
18
|
+
If ``RUNSIGHT_BASE_PATH`` is set, pydantic-settings will use it directly
|
|
19
|
+
and this default is never called. Otherwise we run the marker / auto-detect
|
|
20
|
+
logic.
|
|
21
|
+
"""
|
|
22
|
+
return resolve_base_path(env_value=None)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_cors_origins(raw: str) -> List[str]:
|
|
26
|
+
"""Parse a comma-separated string into a list of origin URLs."""
|
|
27
|
+
return [origin.strip() for origin in raw.split(",")]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Settings(BaseSettings):
|
|
31
|
+
base_path: str = Field(default_factory=_default_base_path)
|
|
32
|
+
db_url: str = _DB_URL_SENTINEL
|
|
33
|
+
debug: bool = False
|
|
34
|
+
host: str = "0.0.0.0"
|
|
35
|
+
port: int = 8000
|
|
36
|
+
cors_origins: str = "http://localhost:5173"
|
|
37
|
+
log_level: str = "INFO"
|
|
38
|
+
log_format: str = "json"
|
|
39
|
+
|
|
40
|
+
model_config = SettingsConfigDict(env_prefix="RUNSIGHT_")
|
|
41
|
+
|
|
42
|
+
@model_validator(mode="after")
|
|
43
|
+
def _resolve_db_url(self) -> "Settings":
|
|
44
|
+
if self.db_url == _DB_URL_SENTINEL:
|
|
45
|
+
db_path = Path(self.base_path).resolve() / ".runsight" / "runsight.db"
|
|
46
|
+
object.__setattr__(self, "db_url", f"sqlite:///{db_path}")
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
@model_validator(mode="after")
|
|
50
|
+
def _split_cors_origins(self) -> "Settings":
|
|
51
|
+
raw = self.cors_origins
|
|
52
|
+
if isinstance(raw, str):
|
|
53
|
+
object.__setattr__(self, "cors_origins", _parse_cors_origins(raw))
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ensure_project_dirs(settings: Settings) -> None:
|
|
58
|
+
"""Ensure the custom/workflows/, .canvas/, and .runsight/ directories exist.
|
|
59
|
+
|
|
60
|
+
Called once at application startup. Resolves base_path to an absolute
|
|
61
|
+
path and logs the result. Creates missing directories as needed.
|
|
62
|
+
"""
|
|
63
|
+
resolved = Path(settings.base_path).resolve()
|
|
64
|
+
logger.info("Runsight base_path resolved to: %s", resolved)
|
|
65
|
+
|
|
66
|
+
workflows_dir = resolved / "custom" / "workflows"
|
|
67
|
+
canvas_dir = workflows_dir / ".canvas"
|
|
68
|
+
runsight_dir = resolved / ".runsight"
|
|
69
|
+
|
|
70
|
+
for d in (workflows_dir, canvas_dir, runsight_dir):
|
|
71
|
+
if not d.exists():
|
|
72
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
logger.info("Created missing directory: %s", d)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
settings = Settings()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""ContextVars for request/execution tracing, bridged to structlog."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
request_id: ContextVar[str] = ContextVar("request_id", default="")
|
|
9
|
+
run_id: ContextVar[str] = ContextVar("run_id", default="")
|
|
10
|
+
block_id: ContextVar[str] = ContextVar("block_id", default="")
|
|
11
|
+
workflow_name: ContextVar[str] = ContextVar("workflow_name", default="")
|
|
12
|
+
|
|
13
|
+
# Keep references that won't be shadowed by parameter names
|
|
14
|
+
_module = sys.modules[__name__]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def bind_request_context(rid: str) -> None:
|
|
18
|
+
"""Set request_id in both ContextVar and structlog context."""
|
|
19
|
+
request_id.set(rid)
|
|
20
|
+
structlog.contextvars.bind_contextvars(request_id=rid)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def bind_execution_context(*, run_id: str, workflow_name: str) -> None:
|
|
24
|
+
"""Set run_id and workflow_name in both ContextVar and structlog context."""
|
|
25
|
+
getattr(_module, "run_id").set(run_id)
|
|
26
|
+
getattr(_module, "workflow_name").set(workflow_name)
|
|
27
|
+
structlog.contextvars.bind_contextvars(run_id=run_id, workflow_name=workflow_name)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def bind_block_context(bid: str) -> None:
|
|
31
|
+
"""Set block_id in both ContextVar and structlog context."""
|
|
32
|
+
block_id.set(bid)
|
|
33
|
+
structlog.contextvars.bind_contextvars(block_id=bid)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def clear_block_context() -> None:
|
|
37
|
+
"""Reset block_id but leave execution context intact."""
|
|
38
|
+
block_id.set("")
|
|
39
|
+
structlog.contextvars.unbind_contextvars("block_id")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def clear_execution_context() -> None:
|
|
43
|
+
"""Reset run_id, workflow_name, and block_id."""
|
|
44
|
+
run_id.set("")
|
|
45
|
+
workflow_name.set("")
|
|
46
|
+
block_id.set("")
|
|
47
|
+
structlog.contextvars.unbind_contextvars("run_id", "workflow_name", "block_id")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from sqlmodel import create_engine
|
|
2
|
+
|
|
3
|
+
from .config import settings
|
|
4
|
+
|
|
5
|
+
# SQLite :memory: needs check_same_thread=False for TestClient/async usage
|
|
6
|
+
connect_args = {}
|
|
7
|
+
if ":memory:" in settings.db_url:
|
|
8
|
+
connect_args["check_same_thread"] = False
|
|
9
|
+
|
|
10
|
+
engine = create_engine(
|
|
11
|
+
settings.db_url,
|
|
12
|
+
echo=settings.debug,
|
|
13
|
+
connect_args=connect_args if connect_args else {},
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Container:
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.engine = engine
|
|
20
|
+
|
|
21
|
+
def setup_app_state(self, app):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
container = Container()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Structured logging configuration using structlog + stdlib bridge."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def configure_logging(log_level: str, log_format: str) -> None:
|
|
10
|
+
"""Configure structlog with JSON or console rendering and stdlib bridge.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
log_level: Python log level name (e.g. "INFO", "DEBUG").
|
|
14
|
+
log_format: "json" for machine-readable output, "text" for human-readable.
|
|
15
|
+
"""
|
|
16
|
+
timestamper = structlog.processors.TimeStamper(fmt="iso")
|
|
17
|
+
|
|
18
|
+
shared_processors: list[structlog.types.Processor] = [
|
|
19
|
+
structlog.contextvars.merge_contextvars,
|
|
20
|
+
structlog.stdlib.add_log_level,
|
|
21
|
+
structlog.stdlib.add_logger_name,
|
|
22
|
+
timestamper,
|
|
23
|
+
structlog.processors.StackInfoRenderer(),
|
|
24
|
+
structlog.processors.format_exc_info,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
if log_format == "json":
|
|
28
|
+
renderer: structlog.types.Processor = structlog.processors.JSONRenderer()
|
|
29
|
+
else:
|
|
30
|
+
renderer = structlog.dev.ConsoleRenderer()
|
|
31
|
+
|
|
32
|
+
# Configure structlog to route through stdlib logging
|
|
33
|
+
structlog.configure(
|
|
34
|
+
processors=[
|
|
35
|
+
*shared_processors,
|
|
36
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
37
|
+
],
|
|
38
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
39
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
40
|
+
cache_logger_on_first_use=False,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Configure stdlib logging with structlog formatter
|
|
44
|
+
formatter = structlog.stdlib.ProcessorFormatter(
|
|
45
|
+
processors=[
|
|
46
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
47
|
+
renderer,
|
|
48
|
+
],
|
|
49
|
+
foreign_pre_chain=shared_processors,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
53
|
+
handler.setFormatter(formatter)
|
|
54
|
+
|
|
55
|
+
root_logger = logging.getLogger()
|
|
56
|
+
root_logger.handlers.clear()
|
|
57
|
+
root_logger.addHandler(handler)
|
|
58
|
+
root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
|
|
59
|
+
|
|
60
|
+
# Suppress noisy loggers
|
|
61
|
+
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
|
62
|
+
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Project detection: resolve base_path from marker file or directory structure."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
MARKER_FILE = ".runsight-project"
|
|
13
|
+
MAX_WALK_DEPTH = 20
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parse_marker(marker_path: Path) -> Optional[str]:
|
|
17
|
+
"""Parse a .runsight-project YAML file and return the resolved base_path.
|
|
18
|
+
|
|
19
|
+
Returns None if the file is invalid or missing the base_path field.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
text = marker_path.read_text(encoding="utf-8")
|
|
23
|
+
data = yaml.safe_load(text)
|
|
24
|
+
if not isinstance(data, dict) or "base_path" not in data:
|
|
25
|
+
logger.warning("Marker %s missing 'base_path' field, skipping", marker_path)
|
|
26
|
+
return None
|
|
27
|
+
raw = data["base_path"]
|
|
28
|
+
resolved = (marker_path.parent / raw).resolve()
|
|
29
|
+
return str(resolved)
|
|
30
|
+
except Exception:
|
|
31
|
+
logger.warning("Failed to parse marker %s, skipping", marker_path, exc_info=True)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _find_marker(start: Path) -> Optional[str]:
|
|
36
|
+
"""Walk up from *start* looking for a .runsight-project file.
|
|
37
|
+
|
|
38
|
+
Returns the resolved base_path string, or None.
|
|
39
|
+
"""
|
|
40
|
+
current = start.resolve()
|
|
41
|
+
for _ in range(MAX_WALK_DEPTH):
|
|
42
|
+
candidate = current / MARKER_FILE
|
|
43
|
+
try:
|
|
44
|
+
found = candidate.is_file()
|
|
45
|
+
except OSError:
|
|
46
|
+
found = False
|
|
47
|
+
if found:
|
|
48
|
+
result = _parse_marker(candidate)
|
|
49
|
+
if result is not None:
|
|
50
|
+
logger.info("Found project marker at %s", candidate)
|
|
51
|
+
return result
|
|
52
|
+
parent = current.parent
|
|
53
|
+
if parent == current:
|
|
54
|
+
break
|
|
55
|
+
current = parent
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _find_custom_workflows(start: Path) -> Optional[str]:
|
|
60
|
+
"""Walk up from *start* looking for a custom/workflows/ directory.
|
|
61
|
+
|
|
62
|
+
Returns the parent of ``custom/`` as base_path, or None.
|
|
63
|
+
"""
|
|
64
|
+
current = start.resolve()
|
|
65
|
+
for _ in range(MAX_WALK_DEPTH):
|
|
66
|
+
try:
|
|
67
|
+
found = (current / "custom" / "workflows").is_dir()
|
|
68
|
+
except OSError:
|
|
69
|
+
found = False
|
|
70
|
+
if found:
|
|
71
|
+
logger.info("Auto-detected custom/workflows/ at %s", current)
|
|
72
|
+
return str(current)
|
|
73
|
+
parent = current.parent
|
|
74
|
+
if parent == current:
|
|
75
|
+
break
|
|
76
|
+
current = parent
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def scaffold_project(base_path: Path) -> None:
|
|
81
|
+
"""Create or verify the Runsight project structure at *base_path*.
|
|
82
|
+
|
|
83
|
+
Idempotent: existing files/dirs are never overwritten.
|
|
84
|
+
"""
|
|
85
|
+
marker_path = base_path / MARKER_FILE
|
|
86
|
+
is_new = not marker_path.is_file()
|
|
87
|
+
|
|
88
|
+
# Marker file
|
|
89
|
+
if not marker_path.is_file():
|
|
90
|
+
marker_path.write_text(
|
|
91
|
+
yaml.dump({"version": 1, "base_path": "."}, default_flow_style=False),
|
|
92
|
+
encoding="utf-8",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Directories
|
|
96
|
+
(base_path / "custom" / "workflows").mkdir(parents=True, exist_ok=True)
|
|
97
|
+
(base_path / "custom" / "souls").mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
# .gitignore
|
|
100
|
+
gitignore_path = base_path / ".gitignore"
|
|
101
|
+
if not gitignore_path.is_file():
|
|
102
|
+
gitignore_path.write_text(".canvas/\n.runsight/\n", encoding="utf-8")
|
|
103
|
+
else:
|
|
104
|
+
content = gitignore_path.read_text(encoding="utf-8")
|
|
105
|
+
if ".runsight/" not in content:
|
|
106
|
+
with gitignore_path.open("a", encoding="utf-8") as f:
|
|
107
|
+
if content and not content.endswith("\n"):
|
|
108
|
+
f.write("\n")
|
|
109
|
+
f.write(".runsight/\n")
|
|
110
|
+
|
|
111
|
+
# Git init if no repo exists
|
|
112
|
+
if not (base_path / ".git").is_dir():
|
|
113
|
+
subprocess.run(["git", "init"], cwd=base_path, capture_output=True, check=True)
|
|
114
|
+
subprocess.run(
|
|
115
|
+
["git", "config", "user.email", "runsight@localhost"],
|
|
116
|
+
cwd=base_path,
|
|
117
|
+
capture_output=True,
|
|
118
|
+
check=True,
|
|
119
|
+
)
|
|
120
|
+
subprocess.run(
|
|
121
|
+
["git", "config", "user.name", "Runsight"],
|
|
122
|
+
cwd=base_path,
|
|
123
|
+
capture_output=True,
|
|
124
|
+
check=True,
|
|
125
|
+
)
|
|
126
|
+
subprocess.run(["git", "add", "."], cwd=base_path, capture_output=True, check=True)
|
|
127
|
+
subprocess.run(
|
|
128
|
+
["git", "commit", "-m", "Initial Runsight project"],
|
|
129
|
+
cwd=base_path,
|
|
130
|
+
capture_output=True,
|
|
131
|
+
check=True,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if is_new:
|
|
135
|
+
logger.info("Created new Runsight project at %s", base_path)
|
|
136
|
+
else:
|
|
137
|
+
logger.info("Found existing Runsight project at %s", base_path)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def resolve_base_path(env_value: Optional[str] = None) -> str:
|
|
141
|
+
"""Resolve the project base_path using a 4-tier priority.
|
|
142
|
+
|
|
143
|
+
1. ``RUNSIGHT_BASE_PATH`` env var (passed in as *env_value*)
|
|
144
|
+
2. ``.runsight-project`` marker file (walk up from CWD)
|
|
145
|
+
3. Auto-detect ``custom/workflows/`` directory in ancestors
|
|
146
|
+
4. CWD as last resort
|
|
147
|
+
"""
|
|
148
|
+
# Tier 1: explicit env var
|
|
149
|
+
if env_value is not None:
|
|
150
|
+
logger.info("base_path from RUNSIGHT_BASE_PATH env var: %s", env_value)
|
|
151
|
+
return env_value
|
|
152
|
+
|
|
153
|
+
cwd = Path.cwd()
|
|
154
|
+
|
|
155
|
+
# Tier 2: marker file
|
|
156
|
+
marker_result = _find_marker(cwd)
|
|
157
|
+
if marker_result is not None:
|
|
158
|
+
return marker_result
|
|
159
|
+
|
|
160
|
+
# Tier 3: auto-detect custom/workflows/
|
|
161
|
+
auto_result = _find_custom_workflows(cwd)
|
|
162
|
+
if auto_result is not None:
|
|
163
|
+
return auto_result
|
|
164
|
+
|
|
165
|
+
# Tier 4: CWD fallback
|
|
166
|
+
logger.info("No project marker or custom/workflows/ found, using CWD: %s", cwd)
|
|
167
|
+
return str(cwd)
|