@hybridlabor-api/aos 4.4.1 → 4.4.2-beta.1
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.
- package/.agents/skills/firecrawl/SKILL.md +149 -0
- package/.agents/skills/firecrawl/rules/install.md +82 -0
- package/.agents/skills/firecrawl/rules/security.md +26 -0
- package/.agents/skills/firecrawl-agent/SKILL.md +58 -0
- package/.agents/skills/firecrawl-build/SKILL.md +39 -0
- package/.agents/skills/firecrawl-build-interact/SKILL.md +68 -0
- package/.agents/skills/firecrawl-build-onboarding/SKILL.md +103 -0
- package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +39 -0
- package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +20 -0
- package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +17 -0
- package/.agents/skills/firecrawl-build-scrape/SKILL.md +69 -0
- package/.agents/skills/firecrawl-build-search/SKILL.md +69 -0
- package/.agents/skills/firecrawl-crawl/SKILL.md +59 -0
- package/.agents/skills/firecrawl-download/SKILL.md +70 -0
- package/.agents/skills/firecrawl-interact/SKILL.md +84 -0
- package/.agents/skills/firecrawl-map/SKILL.md +51 -0
- package/.agents/skills/firecrawl-scrape/SKILL.md +69 -0
- package/.agents/skills/firecrawl-search/SKILL.md +60 -0
- package/CLAUDE.md +573 -0
- package/README.de.md +0 -8
- package/README.md +0 -8
- package/README.pt.md +0 -8
- package/assets/brand/ao-ant-mark.svg +91 -0
- package/assets/brand/bdb-core-mark.svg +34 -0
- package/assets/brand/heimdall-guardian-mark.svg +16 -0
- package/assets/brand/memb-crystal-mark.svg +18 -0
- package/bin/aos-config.mjs +89 -0
- package/bin/aos-dashboard.mjs +608 -0
- package/bin/aos-uninstall.mjs +227 -0
- package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
- package/docs/skills_table.md +0 -3
- package/installer.js +471 -237
- package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
- package/mcps/after-effects-mcp/build/index.js +840 -0
- package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
- package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
- package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
- package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
- package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
- package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
- package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
- package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
- package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
- package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
- package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
- package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
- package/mcps/computer-use-mcp/dist/client.js +136 -0
- package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
- package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
- package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
- package/mcps/computer-use-mcp/dist/native.js +50 -0
- package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
- package/mcps/computer-use-mcp/dist/server.js +342 -0
- package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
- package/mcps/computer-use-mcp/dist/session.js +2372 -0
- package/package.json +13 -4
- package/skills/global_config/aos-project-init/SKILL.md +97 -130
- package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
- package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
- package/skills/global_config/aos-setup/SKILL.md +98 -8
- package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +21 -2
- package/skills/global_config/ask-tim/SKILL.md +2 -7
- package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
- package/bin/setup-saas.mjs +0 -792
- package/mcps/blender-mcp-server/.dockerignore +0 -19
- package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
- package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
- package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
- package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
- package/mcps/blender-mcp-server/Dockerfile +0 -19
- package/mcps/blender-mcp-server/LICENSE +0 -21
- package/mcps/blender-mcp-server/README.md +0 -409
- package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
- package/mcps/blender-mcp-server/addon/models.py +0 -223
- package/mcps/blender-mcp-server/pyproject.toml +0 -120
- package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
- package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
- package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
- package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
- package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
- package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
- package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
- package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
- package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
- package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
- package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
- package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
- package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
- package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
- package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
- package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
- package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
- package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
- package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
- package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
- package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
- package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
- package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
- package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
- package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
- package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
- package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
- package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
- package/mcps/davinci-mcp-professional/.bandit +0 -35
- package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
- package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
- package/mcps/davinci-mcp-professional/.editorconfig +0 -27
- package/mcps/davinci-mcp-professional/.gitattributes +0 -207
- package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
- package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
- package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
- package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
- package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
- package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
- package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
- package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
- package/mcps/davinci-mcp-professional/BUGS.md +0 -55
- package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
- package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
- package/mcps/davinci-mcp-professional/COPYING.md +0 -228
- package/mcps/davinci-mcp-professional/Doxyfile +0 -379
- package/mcps/davinci-mcp-professional/README.md +0 -143
- package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
- package/mcps/davinci-mcp-professional/USING.md +0 -301
- package/mcps/davinci-mcp-professional/VERSION.md +0 -13
- package/mcps/davinci-mcp-professional/build.py +0 -169
- package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
- package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
- package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
- package/mcps/davinci-mcp-professional/main.py +0 -40
- package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
- package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
- package/mcps/davinci-mcp-professional/security_audit.py +0 -379
- package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
- package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
- package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
- package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
- package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
- package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
- package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
- package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
- package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
- package/mcps/davinci-mcp-professional/test.py +0 -186
- package/mcps/davinci-mcp-professional/uv.lock +0 -2073
- package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
- package/mcps/davinci-resolve-mcp/.clinerules +0 -17
- package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
- package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
- package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
- package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
- package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
- package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
- package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
- package/mcps/davinci-resolve-mcp/LICENSE +0 -21
- package/mcps/davinci-resolve-mcp/README.md +0 -173
- package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
- package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
- package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
- package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
- package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
- package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
- package/mcps/davinci-resolve-mcp/install.py +0 -1687
- package/mcps/davinci-resolve-mcp/package.json +0 -52
- package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
- package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
- package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
- package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
- package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
- package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
- package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
- package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
- package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
- package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
- package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
- package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
- package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
- package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
- package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
- package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
- package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
- package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
- package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
- package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
- package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
- package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
- package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
- package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
- package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
- package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
- package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
- package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
- package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
- package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
- package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
- package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
- package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
- package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
- package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
- package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
- package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
- package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
- package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
- package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
- package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
- package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
- package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
- package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
- package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
- package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
- package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
- package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
- package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
- package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
- package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
- package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
- package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
- package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
- package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
- package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
- package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
- package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
- package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
- package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
- package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
- package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
- package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
- package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
- package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
- package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
- package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
- package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
- package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
- package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
- package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
- package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
- package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
- package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
- package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
- package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
- package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
- package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
- package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
- package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
- package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
- package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
- package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
- package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
- package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
- package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
- package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
- package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
- package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
- package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
- package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
- package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
- package/mcps/vectorworks-mcp/.dockerignore +0 -8
- package/mcps/vectorworks-mcp/Dockerfile +0 -28
- package/mcps/vectorworks-mcp/README.md +0 -156
- package/mcps/vectorworks-mcp/app/__init__.py +0 -2
- package/mcps/vectorworks-mcp/app/api.py +0 -52
- package/mcps/vectorworks-mcp/app/chunking.py +0 -56
- package/mcps/vectorworks-mcp/app/config.py +0 -29
- package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
- package/mcps/vectorworks-mcp/app/indexer.py +0 -106
- package/mcps/vectorworks-mcp/app/main.py +0 -33
- package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
- package/mcps/vectorworks-mcp/app/search.py +0 -54
- package/mcps/vectorworks-mcp/app/utils.py +0 -14
- package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
- package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
- package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
- package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
- package/mcps/vectorworks-mcp/design.md +0 -57
- package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
- package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
- package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
- package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
- package/mcps/vectorworks-mcp/requirements.txt +0 -10
- package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
- package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
- package/mcps/vectorworks-mcp/templates/index.html +0 -40
- package/scripts/ecosystem-health-audit.js +0 -257
- package/skills/bdb-dev-os-skill/SKILL.md +0 -49
- package/skills/bdb-dev-os-skill/package.json +0 -11
- package/skills/bdbsaastraining/SKILL.md +0 -316
- package/skills/bdbsaastraining/references/exam-pool.md +0 -313
- package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
- package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
- package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
- package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
- package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
- package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
- package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
- package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
- package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
- package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
- package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
- package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
- package/skills/global_config/bdbsaashost/SKILL.md +0 -125
|
@@ -1,1687 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
DaVinci Resolve MCP Server — Universal Installer
|
|
4
|
-
|
|
5
|
-
Supports: macOS, Windows, Linux
|
|
6
|
-
Configures: Claude Desktop, Claude Code, Cursor, VS Code (Copilot),
|
|
7
|
-
Windsurf, Cline, Roo Code, Zed, Continue, OpenCode, and manual setup.
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
python install.py # Interactive mode
|
|
11
|
-
python install.py --clients all # Install all clients non-interactively
|
|
12
|
-
python install.py --clients cursor,claude-desktop --no-venv
|
|
13
|
-
python install.py --help
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import argparse
|
|
17
|
-
import json
|
|
18
|
-
import os
|
|
19
|
-
import platform
|
|
20
|
-
import shutil
|
|
21
|
-
import subprocess
|
|
22
|
-
import sys
|
|
23
|
-
import textwrap
|
|
24
|
-
import time
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
|
|
27
|
-
from src.utils.update_check import (
|
|
28
|
-
clear_update_prompt_preferences,
|
|
29
|
-
check_for_updates,
|
|
30
|
-
ignore_update_version,
|
|
31
|
-
set_update_mode,
|
|
32
|
-
snooze_update_prompt,
|
|
33
|
-
update_prompt_decision,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
VERSION = "2.57.5"
|
|
39
|
-
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
40
|
-
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
41
|
-
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
|
42
|
-
# may fail to connect on 3.13+, but the connection check is the real signal,
|
|
43
|
-
# so we proceed with a heads-up rather than refusing to run.
|
|
44
|
-
SUPPORTED_PYTHON_MIN = (3, 10)
|
|
45
|
-
PYTHON_ABI_RISK_MIN = (3, 13)
|
|
46
|
-
|
|
47
|
-
# ─── Colors (disabled on Windows cmd without ANSI support) ────────────────────
|
|
48
|
-
|
|
49
|
-
def _supports_color():
|
|
50
|
-
if os.environ.get("NO_COLOR"):
|
|
51
|
-
return False
|
|
52
|
-
if platform.system() == "Windows":
|
|
53
|
-
return os.environ.get("WT_SESSION") or os.environ.get("TERM_PROGRAM")
|
|
54
|
-
return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
55
|
-
|
|
56
|
-
USE_COLOR = _supports_color()
|
|
57
|
-
|
|
58
|
-
def _c(code, text):
|
|
59
|
-
return f"\033[{code}m{text}\033[0m" if USE_COLOR else text
|
|
60
|
-
|
|
61
|
-
def green(t): return _c("32", t)
|
|
62
|
-
def yellow(t): return _c("33", t)
|
|
63
|
-
def red(t): return _c("31", t)
|
|
64
|
-
def bold(t): return _c("1", t)
|
|
65
|
-
def dim(t): return _c("2", t)
|
|
66
|
-
def cyan(t): return _c("36", t)
|
|
67
|
-
|
|
68
|
-
# ─── Platform Detection ──────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
SYSTEM = platform.system() # Darwin, Windows, Linux
|
|
71
|
-
|
|
72
|
-
def is_mac(): return SYSTEM == "Darwin"
|
|
73
|
-
def is_windows(): return SYSTEM == "Windows"
|
|
74
|
-
def is_linux(): return SYSTEM == "Linux"
|
|
75
|
-
|
|
76
|
-
def platform_name():
|
|
77
|
-
if is_mac(): return "macOS"
|
|
78
|
-
if is_windows(): return "Windows"
|
|
79
|
-
if is_linux(): return "Linux"
|
|
80
|
-
return SYSTEM
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def is_supported_python_version(version):
|
|
84
|
-
major, minor = version[:2]
|
|
85
|
-
return major == 3 and minor >= SUPPORTED_PYTHON_MIN[1]
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def is_abi_risk_python_version(version):
|
|
89
|
-
major, minor = version[:2]
|
|
90
|
-
return major == 3 and minor >= PYTHON_ABI_RISK_MIN[1]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def format_python_version(version):
|
|
94
|
-
return ".".join(str(part) for part in version[:3])
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def python_requirement_text():
|
|
98
|
-
return f"Python {SUPPORTED_PYTHON_MIN[0]}.{SUPPORTED_PYTHON_MIN[1]} or newer"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
_ABI_NOTE_PRINTED = False
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def print_abi_risk_note_once(version, label="Python"):
|
|
105
|
-
"""Emit the 3.13+ heads-up at most once per installer run."""
|
|
106
|
-
global _ABI_NOTE_PRINTED
|
|
107
|
-
if _ABI_NOTE_PRINTED:
|
|
108
|
-
return
|
|
109
|
-
_ABI_NOTE_PRINTED = True
|
|
110
|
-
print(f" {yellow(label + ':')} {python_abi_risk_note(version)}")
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def python_abi_risk_note(version):
|
|
114
|
-
return (
|
|
115
|
-
f"Using Python {format_python_version(version)}. Verified working on recent "
|
|
116
|
-
f"Resolve builds (Studio 20.3.2). If Resolve fails to connect "
|
|
117
|
-
f"(scriptapp(\"Resolve\") returns None), install Python 3.10-3.12 and re-run "
|
|
118
|
-
f"with it, e.g.: python3.12 install.py"
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def python_fix_hint():
|
|
123
|
-
return (
|
|
124
|
-
" How to fix:\n"
|
|
125
|
-
" - Install Python 3.12 (the lowest-risk version for Resolve), e.g.:\n"
|
|
126
|
-
" macOS: brew install python@3.12 (or: pyenv install 3.12)\n"
|
|
127
|
-
" Linux: pyenv install 3.12 (or your distro's python3.12 package)\n"
|
|
128
|
-
" Windows: install Python 3.12 from python.org\n"
|
|
129
|
-
" - Re-run with that interpreter, e.g.: python3.12 install.py"
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _version_for_python(python_path):
|
|
134
|
-
script = (
|
|
135
|
-
"import sys; "
|
|
136
|
-
"print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"
|
|
137
|
-
)
|
|
138
|
-
result = subprocess.run(
|
|
139
|
-
[str(python_path), "-c", script],
|
|
140
|
-
capture_output=True,
|
|
141
|
-
text=True,
|
|
142
|
-
timeout=10,
|
|
143
|
-
)
|
|
144
|
-
if result.returncode != 0:
|
|
145
|
-
output = (result.stderr or result.stdout or "").strip()
|
|
146
|
-
raise RuntimeError(output or f"{python_path} exited with code {result.returncode}")
|
|
147
|
-
parts = result.stdout.strip().split(".")
|
|
148
|
-
if len(parts) < 2:
|
|
149
|
-
raise RuntimeError(f"could not parse Python version from {python_path!s}")
|
|
150
|
-
return tuple(int(part) for part in parts[:3])
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def require_supported_python(python_path, label="Python"):
|
|
154
|
-
try:
|
|
155
|
-
version = _version_for_python(python_path)
|
|
156
|
-
except Exception as exc:
|
|
157
|
-
print(f" {red(label + ':')} Could not inspect {python_path}: {exc}")
|
|
158
|
-
sys.exit(1)
|
|
159
|
-
if not is_supported_python_version(version):
|
|
160
|
-
print(
|
|
161
|
-
f" {red(label + ':')} {python_requirement_text()} is required "
|
|
162
|
-
f"(the MCP SDK needs 3.10+); found {format_python_version(version)} "
|
|
163
|
-
f"at {python_path}"
|
|
164
|
-
)
|
|
165
|
-
print(python_fix_hint())
|
|
166
|
-
sys.exit(1)
|
|
167
|
-
if is_abi_risk_python_version(version):
|
|
168
|
-
print_abi_risk_note_once(version, label)
|
|
169
|
-
return version
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def require_current_python(label="Python"):
|
|
173
|
-
version = (sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
|
|
174
|
-
if not is_supported_python_version(version):
|
|
175
|
-
print(
|
|
176
|
-
f" {red(label + ':')} {python_requirement_text()} is required "
|
|
177
|
-
f"(the MCP SDK needs 3.10+); current interpreter is "
|
|
178
|
-
f"{format_python_version(version)} at {sys.executable}"
|
|
179
|
-
)
|
|
180
|
-
print(python_fix_hint())
|
|
181
|
-
sys.exit(1)
|
|
182
|
-
if is_abi_risk_python_version(version):
|
|
183
|
-
print_abi_risk_note_once(version, label)
|
|
184
|
-
return version
|
|
185
|
-
|
|
186
|
-
# ─── Resolve Path Detection ──────────────────────────────────────────────────
|
|
187
|
-
|
|
188
|
-
RESOLVE_PATHS = {
|
|
189
|
-
"Darwin": {
|
|
190
|
-
"api": [
|
|
191
|
-
"/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting",
|
|
192
|
-
],
|
|
193
|
-
"lib": [
|
|
194
|
-
"/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so",
|
|
195
|
-
],
|
|
196
|
-
"app": [
|
|
197
|
-
"/Applications/DaVinci Resolve/DaVinci Resolve.app",
|
|
198
|
-
],
|
|
199
|
-
},
|
|
200
|
-
"Windows": {
|
|
201
|
-
"api": [
|
|
202
|
-
r"C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Developer\Scripting",
|
|
203
|
-
r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Developer\Scripting",
|
|
204
|
-
],
|
|
205
|
-
"lib": [
|
|
206
|
-
r"C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionscript.dll",
|
|
207
|
-
],
|
|
208
|
-
"app": [
|
|
209
|
-
r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Resolve.exe",
|
|
210
|
-
],
|
|
211
|
-
},
|
|
212
|
-
"Linux": {
|
|
213
|
-
"api": [
|
|
214
|
-
"/opt/resolve/Developer/Scripting",
|
|
215
|
-
"/opt/resolve/libs/Fusion/Developer/Scripting",
|
|
216
|
-
"/home/{user}/.local/share/DaVinciResolve/Developer/Scripting",
|
|
217
|
-
],
|
|
218
|
-
"lib": [
|
|
219
|
-
"/opt/resolve/libs/Fusion/fusionscript.so",
|
|
220
|
-
"/opt/resolve/bin/fusionscript.so",
|
|
221
|
-
],
|
|
222
|
-
"app": [
|
|
223
|
-
"/opt/resolve/bin/resolve",
|
|
224
|
-
],
|
|
225
|
-
},
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def find_resolve_paths():
|
|
230
|
-
"""Auto-detect DaVinci Resolve installation paths."""
|
|
231
|
-
candidates = RESOLVE_PATHS.get(SYSTEM, RESOLVE_PATHS["Linux"])
|
|
232
|
-
username = os.environ.get("USER", os.environ.get("USERNAME", ""))
|
|
233
|
-
|
|
234
|
-
api_path = None
|
|
235
|
-
lib_path = None
|
|
236
|
-
|
|
237
|
-
for p in candidates["api"]:
|
|
238
|
-
expanded = p.replace("{user}", username)
|
|
239
|
-
if os.path.isdir(expanded):
|
|
240
|
-
api_path = expanded
|
|
241
|
-
break
|
|
242
|
-
|
|
243
|
-
for p in candidates["lib"]:
|
|
244
|
-
expanded = p.replace("{user}", username)
|
|
245
|
-
if os.path.isfile(expanded):
|
|
246
|
-
lib_path = expanded
|
|
247
|
-
break
|
|
248
|
-
|
|
249
|
-
return api_path, lib_path
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def check_resolve_running():
|
|
253
|
-
"""Check if DaVinci Resolve is currently running."""
|
|
254
|
-
try:
|
|
255
|
-
if is_mac():
|
|
256
|
-
result = subprocess.run(
|
|
257
|
-
["pgrep", "-f", "DaVinci Resolve"],
|
|
258
|
-
capture_output=True, text=True
|
|
259
|
-
)
|
|
260
|
-
return result.returncode == 0
|
|
261
|
-
elif is_windows():
|
|
262
|
-
result = subprocess.run(
|
|
263
|
-
["tasklist", "/FI", "IMAGENAME eq Resolve.exe"],
|
|
264
|
-
capture_output=True, text=True
|
|
265
|
-
)
|
|
266
|
-
return "Resolve.exe" in result.stdout
|
|
267
|
-
else: # Linux
|
|
268
|
-
result = subprocess.run(
|
|
269
|
-
["pgrep", "-f", "resolve"],
|
|
270
|
-
capture_output=True, text=True
|
|
271
|
-
)
|
|
272
|
-
return result.returncode == 0
|
|
273
|
-
except Exception:
|
|
274
|
-
return False
|
|
275
|
-
|
|
276
|
-
# ─── MCP Client Definitions ──────────────────────────────────────────────────
|
|
277
|
-
|
|
278
|
-
def home():
|
|
279
|
-
return Path.home()
|
|
280
|
-
|
|
281
|
-
def appdata():
|
|
282
|
-
"""Windows %APPDATA% equivalent."""
|
|
283
|
-
return Path(os.environ.get("APPDATA", home() / "AppData" / "Roaming"))
|
|
284
|
-
|
|
285
|
-
def xdg_config():
|
|
286
|
-
"""Linux XDG_CONFIG_HOME or default."""
|
|
287
|
-
return Path(os.environ.get("XDG_CONFIG_HOME", home() / ".config"))
|
|
288
|
-
|
|
289
|
-
def vscode_global_storage():
|
|
290
|
-
"""VS Code global storage path per platform."""
|
|
291
|
-
if is_mac():
|
|
292
|
-
return home() / "Library" / "Application Support" / "Code" / "User" / "globalStorage"
|
|
293
|
-
elif is_windows():
|
|
294
|
-
return appdata() / "Code" / "User" / "globalStorage"
|
|
295
|
-
else:
|
|
296
|
-
return xdg_config() / "Code" / "User" / "globalStorage"
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
# Each client entry:
|
|
300
|
-
# id, name, config_path_fn, config_key, merge_strategy, notes
|
|
301
|
-
# config_path_fn returns the path; config_key is the JSON key wrapping the server entry
|
|
302
|
-
# merge_strategy: "merge" = add to existing JSON; "create" = create if not exists
|
|
303
|
-
|
|
304
|
-
MCP_CLIENTS = [
|
|
305
|
-
{
|
|
306
|
-
"id": "antigravity",
|
|
307
|
-
"name": "Antigravity",
|
|
308
|
-
"get_path": lambda: home() / ".gemini" / "antigravity" / "mcp_config.json",
|
|
309
|
-
"config_key": "mcpServers",
|
|
310
|
-
"notes": "Google's agentic AI coding assistant (VS Code fork)",
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
"id": "claude-desktop",
|
|
314
|
-
"name": "Claude Desktop",
|
|
315
|
-
"get_path": lambda: {
|
|
316
|
-
"Darwin": home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json",
|
|
317
|
-
"Windows": appdata() / "Claude" / "claude_desktop_config.json",
|
|
318
|
-
"Linux": xdg_config() / "Claude" / "claude_desktop_config.json",
|
|
319
|
-
}.get(SYSTEM),
|
|
320
|
-
"config_key": "mcpServers",
|
|
321
|
-
"notes": "Anthropic's desktop app for Claude",
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
"id": "claude-code",
|
|
325
|
-
"name": "Claude Code",
|
|
326
|
-
"get_path": lambda: Path.cwd() / ".mcp.json",
|
|
327
|
-
"config_key": "mcpServers",
|
|
328
|
-
"notes": "Project-scoped config (committed to repo)",
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
"id": "cursor",
|
|
332
|
-
"name": "Cursor",
|
|
333
|
-
"get_path": lambda: {
|
|
334
|
-
"Darwin": home() / ".cursor" / "mcp.json",
|
|
335
|
-
"Windows": home() / ".cursor" / "mcp.json",
|
|
336
|
-
"Linux": home() / ".cursor" / "mcp.json",
|
|
337
|
-
}.get(SYSTEM),
|
|
338
|
-
"config_key": "mcpServers",
|
|
339
|
-
"notes": "AI-native code editor (VS Code fork)",
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
"id": "vscode",
|
|
343
|
-
"name": "VS Code (GitHub Copilot)",
|
|
344
|
-
"get_path": lambda: Path.cwd() / ".vscode" / "mcp.json",
|
|
345
|
-
"config_key": "servers",
|
|
346
|
-
"notes": "Workspace-scoped config for Copilot agent mode",
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
"id": "windsurf",
|
|
350
|
-
"name": "Windsurf",
|
|
351
|
-
"get_path": lambda: {
|
|
352
|
-
"Darwin": home() / ".codeium" / "windsurf" / "mcp_config.json",
|
|
353
|
-
"Windows": appdata() / "windsurf" / "mcp_settings.json",
|
|
354
|
-
"Linux": home() / ".codeium" / "windsurf" / "mcp_config.json",
|
|
355
|
-
}.get(SYSTEM),
|
|
356
|
-
"config_key": "mcpServers",
|
|
357
|
-
"notes": "Codeium's AI code editor",
|
|
358
|
-
},
|
|
359
|
-
{
|
|
360
|
-
"id": "cline",
|
|
361
|
-
"name": "Cline",
|
|
362
|
-
"get_path": lambda: vscode_global_storage() / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
363
|
-
"config_key": "mcpServers",
|
|
364
|
-
"notes": "AI coding assistant (VS Code extension)",
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
"id": "roo-code",
|
|
368
|
-
"name": "Roo Code",
|
|
369
|
-
"get_path": lambda: vscode_global_storage() / "rooveterinaryinc.roo-cline" / "settings" / "mcp_settings.json",
|
|
370
|
-
"config_key": "mcpServers",
|
|
371
|
-
"notes": "Autonomous AI coding assistant (VS Code extension)",
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
"id": "zed",
|
|
375
|
-
"name": "Zed",
|
|
376
|
-
"get_path": lambda: {
|
|
377
|
-
"Darwin": home() / ".config" / "zed" / "settings.json",
|
|
378
|
-
"Windows": None, # Zed doesn't support Windows yet
|
|
379
|
-
"Linux": home() / ".config" / "zed" / "settings.json",
|
|
380
|
-
}.get(SYSTEM),
|
|
381
|
-
"config_key": "context_servers",
|
|
382
|
-
"notes": "High-performance code editor (macOS/Linux only)",
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
"id": "continue",
|
|
386
|
-
"name": "Continue",
|
|
387
|
-
"get_path": lambda: {
|
|
388
|
-
"Darwin": home() / ".continue" / "config.json",
|
|
389
|
-
"Windows": home() / ".continue" / "config.json",
|
|
390
|
-
"Linux": home() / ".continue" / "config.json",
|
|
391
|
-
}.get(SYSTEM),
|
|
392
|
-
"config_key": "mcpServers",
|
|
393
|
-
"notes": "Open-source AI code assistant",
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
"id": "opencode",
|
|
397
|
-
"name": "OpenCode",
|
|
398
|
-
# OpenCode uses ~/.config/opencode/opencode.json on every platform
|
|
399
|
-
# (it also reads a project-root opencode.json, but the global file is
|
|
400
|
-
# the safe default for an installer). See https://opencode.ai/docs/config/
|
|
401
|
-
"get_path": lambda: home() / ".config" / "opencode" / "opencode.json",
|
|
402
|
-
"config_key": "mcp",
|
|
403
|
-
"notes": "AI coding agent (uses its own type/enabled/command-array format)",
|
|
404
|
-
},
|
|
405
|
-
]
|
|
406
|
-
|
|
407
|
-
CLIENT_IDS = [c["id"] for c in MCP_CLIENTS]
|
|
408
|
-
|
|
409
|
-
# ─── Server Entry Builder ────────────────────────────────────────────────────
|
|
410
|
-
|
|
411
|
-
def get_python_base_install(python_path):
|
|
412
|
-
"""Resolve the base Python install used by the selected interpreter."""
|
|
413
|
-
script = "import sys; print(sys.base_prefix or sys.prefix)"
|
|
414
|
-
try:
|
|
415
|
-
result = subprocess.run(
|
|
416
|
-
[str(python_path), "-c", script],
|
|
417
|
-
capture_output=True,
|
|
418
|
-
text=True,
|
|
419
|
-
timeout=5,
|
|
420
|
-
)
|
|
421
|
-
base_prefix = result.stdout.strip()
|
|
422
|
-
if result.returncode == 0 and base_prefix:
|
|
423
|
-
return base_prefix
|
|
424
|
-
except Exception as exc:
|
|
425
|
-
print(f"Warning: Could not query Python base prefix: {exc}")
|
|
426
|
-
|
|
427
|
-
resolved = Path(python_path).resolve()
|
|
428
|
-
if resolved.parent.name.lower() in {"scripts", "bin"}:
|
|
429
|
-
return str(resolved.parent.parent)
|
|
430
|
-
return str(resolved.parent)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
def build_server_env(python_path, api_path, lib_path, system=SYSTEM, python_home=None):
|
|
434
|
-
"""Build the env block used by all generated stdio MCP configs."""
|
|
435
|
-
api_value = str(api_path or "")
|
|
436
|
-
lib_value = str(lib_path or "")
|
|
437
|
-
env = {
|
|
438
|
-
"RESOLVE_SCRIPT_API": api_value,
|
|
439
|
-
"RESOLVE_SCRIPT_LIB": lib_value,
|
|
440
|
-
"PYTHONPATH": str(Path(api_value) / "Modules") if api_value else "",
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if system == "Windows":
|
|
444
|
-
env["PYTHONHOME"] = str(python_home or get_python_base_install(python_path))
|
|
445
|
-
|
|
446
|
-
return env
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
def build_server_entry(python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
|
|
450
|
-
"""Build the standard MCP server config entry."""
|
|
451
|
-
return {
|
|
452
|
-
"command": str(python_path),
|
|
453
|
-
"args": [str(server_path)],
|
|
454
|
-
"env": build_server_env(python_path, api_path, lib_path, system=system, python_home=python_home),
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
def build_zed_entry(python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
|
|
459
|
-
"""Build Zed-specific server entry (different format)."""
|
|
460
|
-
return {
|
|
461
|
-
"command": {
|
|
462
|
-
"path": str(python_path),
|
|
463
|
-
"args": [str(server_path)],
|
|
464
|
-
},
|
|
465
|
-
"env": build_server_env(python_path, api_path, lib_path, system=system, python_home=python_home),
|
|
466
|
-
"settings": {},
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def build_opencode_entry(python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
|
|
471
|
-
"""Build OpenCode-specific server entry (issue #72).
|
|
472
|
-
|
|
473
|
-
OpenCode's schema differs from the standard format in three ways: it wraps
|
|
474
|
-
the entry in a ``"mcp"`` key, the interpreter and script are a single
|
|
475
|
-
``"command"`` array (no separate ``args``), and the environment block is
|
|
476
|
-
``"environment"`` rather than ``"env"``. It also expects ``type``/``enabled``
|
|
477
|
-
discriminators. See https://opencode.ai/docs/mcp-servers/
|
|
478
|
-
"""
|
|
479
|
-
return {
|
|
480
|
-
"type": "local",
|
|
481
|
-
"enabled": True,
|
|
482
|
-
"command": [str(python_path), str(server_path)],
|
|
483
|
-
"environment": build_server_env(python_path, api_path, lib_path, system=system, python_home=python_home),
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def build_entry_for_client(client, python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
|
|
488
|
-
"""Return the server entry shaped for a specific client's config schema."""
|
|
489
|
-
builders = {
|
|
490
|
-
"zed": build_zed_entry,
|
|
491
|
-
"opencode": build_opencode_entry,
|
|
492
|
-
}
|
|
493
|
-
builder = builders.get(client["id"], build_server_entry)
|
|
494
|
-
return builder(python_path, server_path, api_path, lib_path, system=system, python_home=python_home)
|
|
495
|
-
|
|
496
|
-
# ─── Config File Operations ──────────────────────────────────────────────────
|
|
497
|
-
|
|
498
|
-
class ConfigParseError(Exception):
|
|
499
|
-
"""Existing config file has content but could not be parsed.
|
|
500
|
-
|
|
501
|
-
Callers must NOT overwrite such a file -- doing so destroys the user's
|
|
502
|
-
settings (issue #71).
|
|
503
|
-
"""
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
def _strip_jsonc(text):
|
|
507
|
-
"""Best-effort strip of // and /* */ comments and trailing commas.
|
|
508
|
-
|
|
509
|
-
Zed's ``settings.json`` (and several other clients) accept JSON-with-comments.
|
|
510
|
-
Python's ``json`` module rejects those, so we strip them before parsing.
|
|
511
|
-
The walk is string-aware, so a ``//`` or ``/*`` sequence inside a JSON
|
|
512
|
-
string value is preserved.
|
|
513
|
-
"""
|
|
514
|
-
out = []
|
|
515
|
-
i = 0
|
|
516
|
-
n = len(text)
|
|
517
|
-
in_string = False
|
|
518
|
-
escape = False
|
|
519
|
-
while i < n:
|
|
520
|
-
ch = text[i]
|
|
521
|
-
if in_string:
|
|
522
|
-
out.append(ch)
|
|
523
|
-
if escape:
|
|
524
|
-
escape = False
|
|
525
|
-
elif ch == "\\":
|
|
526
|
-
escape = True
|
|
527
|
-
elif ch == '"':
|
|
528
|
-
in_string = False
|
|
529
|
-
i += 1
|
|
530
|
-
continue
|
|
531
|
-
if ch == '"':
|
|
532
|
-
in_string = True
|
|
533
|
-
out.append(ch)
|
|
534
|
-
i += 1
|
|
535
|
-
continue
|
|
536
|
-
if ch == "/" and i + 1 < n and text[i + 1] == "/":
|
|
537
|
-
i += 2
|
|
538
|
-
while i < n and text[i] not in "\r\n":
|
|
539
|
-
i += 1
|
|
540
|
-
continue
|
|
541
|
-
if ch == "/" and i + 1 < n and text[i + 1] == "*":
|
|
542
|
-
i += 2
|
|
543
|
-
while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
|
|
544
|
-
i += 1
|
|
545
|
-
i += 2
|
|
546
|
-
continue
|
|
547
|
-
out.append(ch)
|
|
548
|
-
i += 1
|
|
549
|
-
stripped = "".join(out)
|
|
550
|
-
# Drop trailing commas before a closing brace/bracket — string-aware, so a
|
|
551
|
-
# comma inside a string value (e.g. "a, }") is never touched. A prior regex
|
|
552
|
-
# pass here was NOT string-aware and silently corrupted such values during a
|
|
553
|
-
# JSONC merge (follow-up to the issue #71 fix).
|
|
554
|
-
result = []
|
|
555
|
-
i = 0
|
|
556
|
-
n = len(stripped)
|
|
557
|
-
in_string = False
|
|
558
|
-
escape = False
|
|
559
|
-
while i < n:
|
|
560
|
-
ch = stripped[i]
|
|
561
|
-
if in_string:
|
|
562
|
-
result.append(ch)
|
|
563
|
-
if escape:
|
|
564
|
-
escape = False
|
|
565
|
-
elif ch == "\\":
|
|
566
|
-
escape = True
|
|
567
|
-
elif ch == '"':
|
|
568
|
-
in_string = False
|
|
569
|
-
i += 1
|
|
570
|
-
continue
|
|
571
|
-
if ch == '"':
|
|
572
|
-
in_string = True
|
|
573
|
-
result.append(ch)
|
|
574
|
-
i += 1
|
|
575
|
-
continue
|
|
576
|
-
if ch == ",":
|
|
577
|
-
j = i + 1
|
|
578
|
-
while j < n and stripped[j] in " \t\r\n":
|
|
579
|
-
j += 1
|
|
580
|
-
if j < n and stripped[j] in "}]":
|
|
581
|
-
i += 1 # trailing comma: drop it
|
|
582
|
-
continue
|
|
583
|
-
result.append(ch)
|
|
584
|
-
i += 1
|
|
585
|
-
return "".join(result)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
def read_json(path):
|
|
589
|
-
"""Read a JSON/JSONC config file.
|
|
590
|
-
|
|
591
|
-
Returns an empty dict when the file is absent or empty (safe to create).
|
|
592
|
-
Raises :class:`ConfigParseError` when the file has content that cannot be
|
|
593
|
-
parsed even after stripping JSONC comments and trailing commas -- callers
|
|
594
|
-
must refuse to overwrite such a file, or they would wipe the user's
|
|
595
|
-
existing settings (issue #71: Zed's settings.json ships with comments).
|
|
596
|
-
"""
|
|
597
|
-
try:
|
|
598
|
-
with open(path, "r") as f:
|
|
599
|
-
raw = f.read()
|
|
600
|
-
except FileNotFoundError:
|
|
601
|
-
return {}
|
|
602
|
-
|
|
603
|
-
if not raw.strip():
|
|
604
|
-
return {}
|
|
605
|
-
|
|
606
|
-
try:
|
|
607
|
-
return json.loads(raw)
|
|
608
|
-
except json.JSONDecodeError:
|
|
609
|
-
pass
|
|
610
|
-
|
|
611
|
-
try:
|
|
612
|
-
return json.loads(_strip_jsonc(raw))
|
|
613
|
-
except json.JSONDecodeError as exc:
|
|
614
|
-
raise ConfigParseError(str(exc)) from exc
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
def write_json(path, data):
|
|
618
|
-
"""Write JSON to file, creating parent directories."""
|
|
619
|
-
path = Path(path)
|
|
620
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
621
|
-
|
|
622
|
-
# Backup existing file
|
|
623
|
-
if path.exists():
|
|
624
|
-
backup = path.with_suffix(path.suffix + ".backup")
|
|
625
|
-
shutil.copy2(path, backup)
|
|
626
|
-
|
|
627
|
-
with open(path, "w") as f:
|
|
628
|
-
json.dump(data, f, indent=2)
|
|
629
|
-
f.write("\n")
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
def write_client_config(client, python_path, server_path, api_path, lib_path, dry_run=False):
|
|
633
|
-
"""Write or merge MCP config for a specific client. Returns (success, message)."""
|
|
634
|
-
config_path = client["get_path"]()
|
|
635
|
-
if config_path is None:
|
|
636
|
-
return False, f"{client['name']} is not available on {platform_name()}"
|
|
637
|
-
|
|
638
|
-
config_key = client["config_key"]
|
|
639
|
-
|
|
640
|
-
# Build the server entry (some clients use a non-standard schema)
|
|
641
|
-
server_entry = build_entry_for_client(client, python_path, server_path, api_path, lib_path)
|
|
642
|
-
|
|
643
|
-
if dry_run:
|
|
644
|
-
preview = {config_key: {"davinci-resolve": server_entry}}
|
|
645
|
-
return True, f"Would write to {config_path}:\n{json.dumps(preview, indent=2)}"
|
|
646
|
-
|
|
647
|
-
# Read existing config and merge. If the file exists but cannot be parsed,
|
|
648
|
-
# refuse to overwrite it -- silently replacing it would wipe the user's
|
|
649
|
-
# settings (issue #71, especially Zed's commented settings.json).
|
|
650
|
-
try:
|
|
651
|
-
existing = read_json(config_path)
|
|
652
|
-
except ConfigParseError as exc:
|
|
653
|
-
return False, (
|
|
654
|
-
f"{config_path} exists but could not be parsed ({exc}). "
|
|
655
|
-
f"Refusing to overwrite to avoid data loss. Add the "
|
|
656
|
-
f'"{config_key}" entry manually using the manual config output '
|
|
657
|
-
f"(run with --manual)."
|
|
658
|
-
)
|
|
659
|
-
|
|
660
|
-
if config_key not in existing:
|
|
661
|
-
existing[config_key] = {}
|
|
662
|
-
|
|
663
|
-
existing[config_key]["davinci-resolve"] = server_entry
|
|
664
|
-
|
|
665
|
-
write_json(config_path, existing)
|
|
666
|
-
return True, str(config_path)
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
def generate_manual_config(python_path, server_path, api_path, lib_path):
|
|
670
|
-
"""Generate config snippets for manual setup."""
|
|
671
|
-
entry = build_server_entry(python_path, server_path, api_path, lib_path)
|
|
672
|
-
zed_entry = build_zed_entry(python_path, server_path, api_path, lib_path)
|
|
673
|
-
opencode_entry = build_opencode_entry(python_path, server_path, api_path, lib_path)
|
|
674
|
-
|
|
675
|
-
standard = json.dumps({"mcpServers": {"davinci-resolve": entry}}, indent=2)
|
|
676
|
-
vscode_fmt = json.dumps({"servers": {"davinci-resolve": entry}}, indent=2)
|
|
677
|
-
zed_fmt = json.dumps({"context_servers": {"davinci-resolve": zed_entry}}, indent=2)
|
|
678
|
-
opencode_fmt = json.dumps({"mcp": {"davinci-resolve": opencode_entry}}, indent=2)
|
|
679
|
-
|
|
680
|
-
return standard, vscode_fmt, zed_fmt, opencode_fmt
|
|
681
|
-
|
|
682
|
-
# ─── Virtual Environment ─────────────────────────────────────────────────────
|
|
683
|
-
|
|
684
|
-
def find_python():
|
|
685
|
-
"""Find the best Python 3 executable."""
|
|
686
|
-
candidates = ["python3", "python"]
|
|
687
|
-
for cmd in candidates:
|
|
688
|
-
try:
|
|
689
|
-
result = subprocess.run(
|
|
690
|
-
[cmd, "--version"], capture_output=True, text=True
|
|
691
|
-
)
|
|
692
|
-
if result.returncode == 0 and "Python 3" in result.stdout:
|
|
693
|
-
return cmd
|
|
694
|
-
except FileNotFoundError:
|
|
695
|
-
continue
|
|
696
|
-
return None
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
def create_venv(venv_path):
|
|
700
|
-
"""Create a Python virtual environment."""
|
|
701
|
-
print(f"\n Creating virtual environment at {dim(str(venv_path))}...")
|
|
702
|
-
subprocess.run(
|
|
703
|
-
[sys.executable, "-m", "venv", str(venv_path)],
|
|
704
|
-
check=True
|
|
705
|
-
)
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
def get_venv_python(venv_path):
|
|
709
|
-
"""Get the Python executable inside a venv."""
|
|
710
|
-
if is_windows():
|
|
711
|
-
return venv_path / "Scripts" / "python.exe"
|
|
712
|
-
return venv_path / "bin" / "python"
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
def get_venv_pip(venv_path):
|
|
716
|
-
"""Get the pip executable inside a venv."""
|
|
717
|
-
if is_windows():
|
|
718
|
-
return venv_path / "Scripts" / "pip.exe"
|
|
719
|
-
return venv_path / "bin" / "pip"
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
def install_dependencies(venv_path, project_dir):
|
|
723
|
-
"""Install Python dependencies into the venv."""
|
|
724
|
-
pip = get_venv_pip(venv_path)
|
|
725
|
-
req_file = project_dir / "requirements.txt"
|
|
726
|
-
|
|
727
|
-
print(f" Installing dependencies...")
|
|
728
|
-
|
|
729
|
-
# Install MCP SDK
|
|
730
|
-
subprocess.run(
|
|
731
|
-
[str(pip), "install", "-q", "mcp[cli]"],
|
|
732
|
-
check=True, capture_output=True
|
|
733
|
-
)
|
|
734
|
-
|
|
735
|
-
# Install from requirements.txt if it exists
|
|
736
|
-
if req_file.exists():
|
|
737
|
-
subprocess.run(
|
|
738
|
-
[str(pip), "install", "-q", "-r", str(req_file)],
|
|
739
|
-
check=True, capture_output=True
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
# ─── Connection Verification ─────────────────────────────────────────────────
|
|
743
|
-
|
|
744
|
-
def verify_resolve_connection(python_path, api_path, lib_path):
|
|
745
|
-
"""Try to import DaVinciResolveScript and connect."""
|
|
746
|
-
if not api_path:
|
|
747
|
-
return False, "Resolve API path not found"
|
|
748
|
-
|
|
749
|
-
env = {**os.environ, **build_server_env(python_path, api_path, lib_path)}
|
|
750
|
-
modules_path = env["PYTHONPATH"]
|
|
751
|
-
test_script = textwrap.dedent(f"""\
|
|
752
|
-
import sys
|
|
753
|
-
sys.path.insert(0, {modules_path!r})
|
|
754
|
-
try:
|
|
755
|
-
import DaVinciResolveScript as dvr
|
|
756
|
-
resolve = dvr.scriptapp('Resolve')
|
|
757
|
-
if resolve:
|
|
758
|
-
name = resolve.GetProductName()
|
|
759
|
-
ver = resolve.GetVersionString()
|
|
760
|
-
print(f"CONNECTED: {{name}} {{ver}}")
|
|
761
|
-
else:
|
|
762
|
-
print("IMPORTED_OK: Module loads but Resolve not running or not responding")
|
|
763
|
-
except ImportError as e:
|
|
764
|
-
print(f"IMPORT_ERROR: {{e}}")
|
|
765
|
-
except Exception as e:
|
|
766
|
-
print(f"ERROR: {{e}}")
|
|
767
|
-
""")
|
|
768
|
-
|
|
769
|
-
try:
|
|
770
|
-
result = subprocess.run(
|
|
771
|
-
[str(python_path), "-c", test_script],
|
|
772
|
-
capture_output=True,
|
|
773
|
-
text=True,
|
|
774
|
-
timeout=10,
|
|
775
|
-
env=env,
|
|
776
|
-
)
|
|
777
|
-
output = result.stdout.strip() or result.stderr.strip()
|
|
778
|
-
if output.startswith("CONNECTED:"):
|
|
779
|
-
return True, output.replace("CONNECTED: ", "")
|
|
780
|
-
elif output.startswith("IMPORTED_OK:"):
|
|
781
|
-
return True, "API module loaded (Resolve not running)"
|
|
782
|
-
else:
|
|
783
|
-
if output:
|
|
784
|
-
return False, output
|
|
785
|
-
return False, f"Process exited with code {result.returncode}"
|
|
786
|
-
except subprocess.TimeoutExpired:
|
|
787
|
-
return False, "Connection timed out"
|
|
788
|
-
except Exception as e:
|
|
789
|
-
return False, str(e)
|
|
790
|
-
|
|
791
|
-
# ─── Interactive UI ───────────────────────────────────────────────────────────
|
|
792
|
-
|
|
793
|
-
def print_banner():
|
|
794
|
-
title = f"DaVinci Resolve MCP Server — Installer v{VERSION}"
|
|
795
|
-
subtitle = "32 compound · 329 full · 3 platforms"
|
|
796
|
-
print()
|
|
797
|
-
print(bold(" ╔══════════════════════════════════════════════════════╗"))
|
|
798
|
-
print(bold(f" ║{title:^54}║"))
|
|
799
|
-
print(bold(" ╠══════════════════════════════════════════════════════╣"))
|
|
800
|
-
print(bold(f" ║{subtitle:^54}║"))
|
|
801
|
-
print(bold(" ╚══════════════════════════════════════════════════════╝"))
|
|
802
|
-
print()
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
def print_step(num, total, text):
|
|
806
|
-
print(f"\n {cyan(f'[{num}/{total}]')} {bold(text)}")
|
|
807
|
-
print(f" {'─' * 50}")
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
def print_update_status(project_dir, *, force=False):
|
|
811
|
-
"""Best-effort installer update notice."""
|
|
812
|
-
result = check_for_updates(VERSION, project_dir, timeout=2.0, force=force)
|
|
813
|
-
status = result.get("status")
|
|
814
|
-
if status == "update_available":
|
|
815
|
-
version_note = dim(
|
|
816
|
-
f"v{result.get('current_version')} -> v{result.get('latest_version')}"
|
|
817
|
-
)
|
|
818
|
-
release_note = dim(f"Latest release: {result.get('release_url')}")
|
|
819
|
-
print(
|
|
820
|
-
f" MCP Update: {yellow('Available')} "
|
|
821
|
-
f"{version_note}"
|
|
822
|
-
)
|
|
823
|
-
print(f" {release_note}")
|
|
824
|
-
elif status == "up_to_date":
|
|
825
|
-
print(f" MCP Update: {green('Up to date')} {dim(f'v{VERSION}')}")
|
|
826
|
-
elif status == "current_ahead":
|
|
827
|
-
print(f" MCP Update: {green('Local build ahead of latest release')} {dim(f'v{VERSION}')}")
|
|
828
|
-
elif status == "disabled":
|
|
829
|
-
print(f" MCP Update: {dim('Check disabled')}")
|
|
830
|
-
elif status == "error":
|
|
831
|
-
print(f" MCP Update: {yellow('Could not check')} {dim(str(result.get('error', '')))}")
|
|
832
|
-
return result
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
def _run_git(project_dir, args, timeout=45):
|
|
836
|
-
try:
|
|
837
|
-
return subprocess.run(
|
|
838
|
-
["git", "-C", str(project_dir), *args],
|
|
839
|
-
capture_output=True,
|
|
840
|
-
text=True,
|
|
841
|
-
timeout=timeout,
|
|
842
|
-
)
|
|
843
|
-
except FileNotFoundError:
|
|
844
|
-
return None
|
|
845
|
-
except subprocess.TimeoutExpired as exc:
|
|
846
|
-
return exc
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
def _git_failure_message(result, fallback):
|
|
850
|
-
if result is None:
|
|
851
|
-
return "git is not available on PATH"
|
|
852
|
-
if isinstance(result, subprocess.TimeoutExpired):
|
|
853
|
-
return "git command timed out"
|
|
854
|
-
output = (result.stderr or result.stdout or "").strip()
|
|
855
|
-
return output or fallback
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
def _record_update_history(project_dir, entry):
|
|
859
|
-
"""Append `entry` to `<project_dir>/logs/update_history.json` (best-effort)."""
|
|
860
|
-
import json as _json
|
|
861
|
-
log_dir = os.path.join(project_dir, "logs")
|
|
862
|
-
try:
|
|
863
|
-
os.makedirs(log_dir, exist_ok=True)
|
|
864
|
-
except OSError:
|
|
865
|
-
return
|
|
866
|
-
path = os.path.join(log_dir, "update_history.json")
|
|
867
|
-
history = {"entries": []}
|
|
868
|
-
if os.path.isfile(path):
|
|
869
|
-
try:
|
|
870
|
-
with open(path, "r", encoding="utf-8") as fh:
|
|
871
|
-
loaded = _json.load(fh)
|
|
872
|
-
if isinstance(loaded, dict) and isinstance(loaded.get("entries"), list):
|
|
873
|
-
history = loaded
|
|
874
|
-
except (OSError, ValueError):
|
|
875
|
-
pass
|
|
876
|
-
history.setdefault("entries", []).append(entry)
|
|
877
|
-
# Trim to most recent 200 entries — keep history bounded.
|
|
878
|
-
if len(history["entries"]) > 200:
|
|
879
|
-
history["entries"] = history["entries"][-200:]
|
|
880
|
-
history["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
881
|
-
tmp_path = path + ".tmp"
|
|
882
|
-
try:
|
|
883
|
-
with open(tmp_path, "w", encoding="utf-8") as fh:
|
|
884
|
-
_json.dump(history, fh, indent=2)
|
|
885
|
-
os.replace(tmp_path, path)
|
|
886
|
-
except OSError:
|
|
887
|
-
try:
|
|
888
|
-
os.remove(tmp_path)
|
|
889
|
-
except OSError:
|
|
890
|
-
pass
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
def _read_current_version(project_dir):
|
|
894
|
-
"""Best-effort read of VERSION constant from src/server.py."""
|
|
895
|
-
server_path = os.path.join(project_dir, "src", "server.py")
|
|
896
|
-
try:
|
|
897
|
-
with open(server_path, "r", encoding="utf-8") as fh:
|
|
898
|
-
for line in fh:
|
|
899
|
-
if line.startswith("VERSION = "):
|
|
900
|
-
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
901
|
-
except OSError:
|
|
902
|
-
pass
|
|
903
|
-
return None
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
def _record_attempt(project_dir, *, kind, success, reason=None, message=None,
|
|
907
|
-
from_version=None, to_version=None, from_sha=None, to_sha=None,
|
|
908
|
-
initiator=None, extra=None):
|
|
909
|
-
"""Append a structured row to update_history.json. `extra` is merged in flat."""
|
|
910
|
-
entry = {
|
|
911
|
-
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
912
|
-
"kind": kind, # "update" | "rollback" | "dry_run"
|
|
913
|
-
"success": bool(success),
|
|
914
|
-
"reason": reason,
|
|
915
|
-
"message": message,
|
|
916
|
-
"from_version": from_version,
|
|
917
|
-
"to_version": to_version,
|
|
918
|
-
"from_sha": from_sha,
|
|
919
|
-
"to_sha": to_sha,
|
|
920
|
-
"initiator": initiator,
|
|
921
|
-
}
|
|
922
|
-
if isinstance(extra, dict):
|
|
923
|
-
entry.update(extra)
|
|
924
|
-
_record_update_history(project_dir, entry)
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
def apply_safe_self_update(project_dir, dry_run=False, *, initiator="cli",
|
|
928
|
-
strategy="refuse_on_dirty"):
|
|
929
|
-
"""Apply a guarded git fast-forward update.
|
|
930
|
-
|
|
931
|
-
`strategy` controls behavior when the working tree is dirty:
|
|
932
|
-
- `"refuse_on_dirty"` (default) — return reason="local_changes" without
|
|
933
|
-
touching anything.
|
|
934
|
-
- `"stash_if_needed"` — `git stash push`, apply the update, `git stash pop`.
|
|
935
|
-
If pop conflicts, leave the stash in place and return reason="stash_pop_conflict"
|
|
936
|
-
with the stash ref so the user can resolve.
|
|
937
|
-
|
|
938
|
-
Every attempt — success or failure — is recorded in
|
|
939
|
-
`<project_dir>/logs/update_history.json` so the dashboard can show what
|
|
940
|
-
happened and rollback can find the prior SHA.
|
|
941
|
-
"""
|
|
942
|
-
inside = _run_git(project_dir, ["rev-parse", "--is-inside-work-tree"])
|
|
943
|
-
if inside is None or isinstance(inside, subprocess.TimeoutExpired) or inside.returncode != 0:
|
|
944
|
-
msg = _git_failure_message(inside, "not a git checkout")
|
|
945
|
-
_record_attempt(project_dir, kind="update", success=False, reason="not_git", message=msg, initiator=initiator)
|
|
946
|
-
return {"success": False, "reason": "not_git", "message": msg}
|
|
947
|
-
|
|
948
|
-
status = _run_git(project_dir, ["status", "--porcelain"])
|
|
949
|
-
if status is None or isinstance(status, subprocess.TimeoutExpired) or status.returncode != 0:
|
|
950
|
-
msg = _git_failure_message(status, "could not inspect git status")
|
|
951
|
-
_record_attempt(project_dir, kind="update", success=False, reason="status_failed", message=msg, initiator=initiator)
|
|
952
|
-
return {"success": False, "reason": "status_failed", "message": msg}
|
|
953
|
-
|
|
954
|
-
stash_ref = None
|
|
955
|
-
if status.stdout.strip():
|
|
956
|
-
if strategy != "stash_if_needed":
|
|
957
|
-
msg = "local changes are present; continuing with the current build"
|
|
958
|
-
_record_attempt(project_dir, kind="update", success=False, reason="local_changes", message=msg, initiator=initiator)
|
|
959
|
-
return {"success": False, "reason": "local_changes", "message": msg}
|
|
960
|
-
# Auto-stash path: push the working tree onto the stash, remember the
|
|
961
|
-
# ref so we can pop (or surface) it after the update.
|
|
962
|
-
stash_msg = f"mcp-update-autostash-{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}"
|
|
963
|
-
stash = _run_git(project_dir, ["stash", "push", "-u", "-m", stash_msg], timeout=30)
|
|
964
|
-
if stash is None or stash.returncode != 0 or "No local changes to save" in (stash.stdout or ""):
|
|
965
|
-
# If we got here `status` was dirty, so a stash failure means trouble.
|
|
966
|
-
msg = _git_failure_message(stash, "git stash push failed")
|
|
967
|
-
_record_attempt(project_dir, kind="update", success=False, reason="stash_failed", message=msg, initiator=initiator)
|
|
968
|
-
return {"success": False, "reason": "stash_failed", "message": msg}
|
|
969
|
-
stash_ref = stash_msg
|
|
970
|
-
|
|
971
|
-
upstream = _run_git(project_dir, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
|
|
972
|
-
if upstream is None or isinstance(upstream, subprocess.TimeoutExpired) or upstream.returncode != 0:
|
|
973
|
-
msg = _git_failure_message(upstream, "current branch has no configured upstream")
|
|
974
|
-
_record_attempt(project_dir, kind="update", success=False, reason="no_upstream", message=msg, initiator=initiator)
|
|
975
|
-
return {"success": False, "reason": "no_upstream", "message": msg}
|
|
976
|
-
|
|
977
|
-
# Capture pre-update SHA + version so rollback knows where to revert to.
|
|
978
|
-
head_before = _run_git(project_dir, ["rev-parse", "HEAD"])
|
|
979
|
-
from_sha = head_before.stdout.strip() if head_before and head_before.returncode == 0 else None
|
|
980
|
-
from_version = _read_current_version(project_dir)
|
|
981
|
-
|
|
982
|
-
if dry_run:
|
|
983
|
-
msg = f"would fast-forward from {upstream.stdout.strip()}"
|
|
984
|
-
_record_attempt(project_dir, kind="dry_run", success=True, message=msg,
|
|
985
|
-
from_version=from_version, from_sha=from_sha, initiator=initiator)
|
|
986
|
-
return {"success": True, "changed": False, "dry_run": True, "message": msg}
|
|
987
|
-
|
|
988
|
-
fetch = _run_git(project_dir, ["fetch", "--tags", "--prune"], timeout=90)
|
|
989
|
-
if fetch is None or isinstance(fetch, subprocess.TimeoutExpired) or fetch.returncode != 0:
|
|
990
|
-
msg = _git_failure_message(fetch, "git fetch failed")
|
|
991
|
-
_record_attempt(project_dir, kind="update", success=False, reason="fetch_failed", message=msg,
|
|
992
|
-
from_version=from_version, from_sha=from_sha, initiator=initiator)
|
|
993
|
-
return {"success": False, "reason": "fetch_failed", "message": msg}
|
|
994
|
-
|
|
995
|
-
pull = _run_git(project_dir, ["pull", "--ff-only"], timeout=120)
|
|
996
|
-
if pull is None or isinstance(pull, subprocess.TimeoutExpired) or pull.returncode != 0:
|
|
997
|
-
msg = _git_failure_message(pull, "git pull --ff-only failed")
|
|
998
|
-
_record_attempt(project_dir, kind="update", success=False, reason="pull_failed", message=msg,
|
|
999
|
-
from_version=from_version, from_sha=from_sha, initiator=initiator)
|
|
1000
|
-
return {"success": False, "reason": "pull_failed", "message": msg}
|
|
1001
|
-
|
|
1002
|
-
head_after = _run_git(project_dir, ["rev-parse", "HEAD"])
|
|
1003
|
-
to_sha = head_after.stdout.strip() if head_after and head_after.returncode == 0 else None
|
|
1004
|
-
to_version = _read_current_version(project_dir)
|
|
1005
|
-
|
|
1006
|
-
output = "\n".join(part.strip() for part in (pull.stdout, pull.stderr) if part and part.strip())
|
|
1007
|
-
changed = "Already up to date." not in output
|
|
1008
|
-
|
|
1009
|
-
# Integrity verification: confirm our local HEAD matches what GitHub said
|
|
1010
|
-
# was the target SHA. Mismatch could mean: (a) user pushed to a fork mid-
|
|
1011
|
-
# update, (b) the release we resolved was different from the branch tip
|
|
1012
|
-
# (force-pushed), (c) corruption. We don't roll back automatically — the
|
|
1013
|
-
# user may have intentional reasons — but we log it loudly.
|
|
1014
|
-
integrity = {"verified": None, "expected_sha": None, "actual_sha": to_sha}
|
|
1015
|
-
try:
|
|
1016
|
-
from src.utils.update_check import check_for_updates as _cfu # type: ignore
|
|
1017
|
-
info = _cfu(from_version or "0.0.0", project_dir, force=False)
|
|
1018
|
-
expected = info.get("release_target_sha")
|
|
1019
|
-
if expected:
|
|
1020
|
-
integrity["expected_sha"] = expected
|
|
1021
|
-
# Compare prefixes (release SHA may be a tag name or short SHA).
|
|
1022
|
-
ok = bool(to_sha) and (
|
|
1023
|
-
expected == to_sha
|
|
1024
|
-
or (len(expected) >= 7 and to_sha.startswith(expected[:7]))
|
|
1025
|
-
or (len(to_sha) >= 7 and expected.startswith(to_sha[:7]))
|
|
1026
|
-
)
|
|
1027
|
-
integrity["verified"] = ok
|
|
1028
|
-
except Exception as exc:
|
|
1029
|
-
integrity["error"] = f"{type(exc).__name__}: {exc}"
|
|
1030
|
-
|
|
1031
|
-
# If we stashed changes earlier, try to reapply them now.
|
|
1032
|
-
stash_pop_conflict = False
|
|
1033
|
-
if stash_ref:
|
|
1034
|
-
pop = _run_git(project_dir, ["stash", "pop"], timeout=60)
|
|
1035
|
-
if pop is None or pop.returncode != 0:
|
|
1036
|
-
stash_pop_conflict = True
|
|
1037
|
-
|
|
1038
|
-
result = {
|
|
1039
|
-
"success": True, "changed": changed,
|
|
1040
|
-
"message": output or "update complete",
|
|
1041
|
-
"from_version": from_version, "to_version": to_version,
|
|
1042
|
-
"from_sha": from_sha, "to_sha": to_sha,
|
|
1043
|
-
"stash_ref": stash_ref,
|
|
1044
|
-
"stash_pop_conflict": stash_pop_conflict,
|
|
1045
|
-
"integrity": integrity,
|
|
1046
|
-
}
|
|
1047
|
-
if stash_pop_conflict:
|
|
1048
|
-
# Update applied successfully but the stash pop hit a conflict; the
|
|
1049
|
-
# user's changes are still in the stash. Don't fail the overall update,
|
|
1050
|
-
# but surface the conflict prominently.
|
|
1051
|
-
result["reason"] = "stash_pop_conflict"
|
|
1052
|
-
result["remediation"] = (
|
|
1053
|
-
f"Update applied, but your stashed changes ({stash_ref}) conflict with the new build. "
|
|
1054
|
-
"Resolve via `git stash list` + `git stash pop` after restarting; "
|
|
1055
|
-
"use `git stash drop` if you want to discard them."
|
|
1056
|
-
)
|
|
1057
|
-
|
|
1058
|
-
_record_attempt(project_dir, kind="update", success=True,
|
|
1059
|
-
message=output or "update complete",
|
|
1060
|
-
from_version=from_version, to_version=to_version,
|
|
1061
|
-
from_sha=from_sha, to_sha=to_sha, initiator=initiator,
|
|
1062
|
-
extra={"stash_ref": stash_ref,
|
|
1063
|
-
"stash_pop_conflict": stash_pop_conflict,
|
|
1064
|
-
"integrity": integrity})
|
|
1065
|
-
|
|
1066
|
-
return result
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
def rollback_to_previous_build(project_dir, *, initiator="cli"):
|
|
1070
|
-
"""git reset --hard to the from_sha of the most recent successful update.
|
|
1071
|
-
|
|
1072
|
-
Refuses if local changes exist (same guard as apply_safe_self_update) so we
|
|
1073
|
-
never silently lose user work. Records a rollback row in update_history.
|
|
1074
|
-
"""
|
|
1075
|
-
import json as _json
|
|
1076
|
-
history_path = os.path.join(project_dir, "logs", "update_history.json")
|
|
1077
|
-
if not os.path.isfile(history_path):
|
|
1078
|
-
return {"success": False, "reason": "no_history", "message": "no update_history.json found"}
|
|
1079
|
-
try:
|
|
1080
|
-
with open(history_path, "r", encoding="utf-8") as fh:
|
|
1081
|
-
history = _json.load(fh)
|
|
1082
|
-
except (OSError, _json.JSONDecodeError) as exc:
|
|
1083
|
-
return {"success": False, "reason": "history_read_failed", "message": str(exc)}
|
|
1084
|
-
|
|
1085
|
-
# Newest first; find the latest successful "update" entry with a from_sha.
|
|
1086
|
-
candidates = [e for e in reversed(history.get("entries") or [])
|
|
1087
|
-
if e.get("kind") == "update" and e.get("success") and e.get("from_sha")]
|
|
1088
|
-
if not candidates:
|
|
1089
|
-
return {"success": False, "reason": "no_target", "message": "no prior successful update to roll back to"}
|
|
1090
|
-
target = candidates[0]
|
|
1091
|
-
from_sha = target["from_sha"]
|
|
1092
|
-
|
|
1093
|
-
status = _run_git(project_dir, ["status", "--porcelain"])
|
|
1094
|
-
if status is None or status.returncode != 0:
|
|
1095
|
-
return {"success": False, "reason": "status_failed", "message": "could not inspect git status"}
|
|
1096
|
-
if status.stdout.strip():
|
|
1097
|
-
return {"success": False, "reason": "local_changes",
|
|
1098
|
-
"message": "local changes are present; commit or stash before rolling back"}
|
|
1099
|
-
|
|
1100
|
-
head_before = _run_git(project_dir, ["rev-parse", "HEAD"])
|
|
1101
|
-
pre_rollback_sha = head_before.stdout.strip() if head_before and head_before.returncode == 0 else None
|
|
1102
|
-
pre_rollback_version = _read_current_version(project_dir)
|
|
1103
|
-
|
|
1104
|
-
reset = _run_git(project_dir, ["reset", "--hard", from_sha], timeout=60)
|
|
1105
|
-
if reset is None or reset.returncode != 0:
|
|
1106
|
-
msg = _git_failure_message(reset, f"git reset --hard {from_sha} failed")
|
|
1107
|
-
_record_attempt(project_dir, kind="rollback", success=False, reason="reset_failed", message=msg,
|
|
1108
|
-
from_version=pre_rollback_version, from_sha=pre_rollback_sha,
|
|
1109
|
-
to_sha=from_sha, initiator=initiator)
|
|
1110
|
-
return {"success": False, "reason": "reset_failed", "message": msg}
|
|
1111
|
-
|
|
1112
|
-
to_version = _read_current_version(project_dir)
|
|
1113
|
-
_record_attempt(project_dir, kind="rollback", success=True,
|
|
1114
|
-
message=f"rolled back to {from_sha[:10]}",
|
|
1115
|
-
from_version=pre_rollback_version, to_version=to_version,
|
|
1116
|
-
from_sha=pre_rollback_sha, to_sha=from_sha, initiator=initiator)
|
|
1117
|
-
return {"success": True, "changed": pre_rollback_sha != from_sha,
|
|
1118
|
-
"message": f"rolled back to {from_sha[:10]}",
|
|
1119
|
-
"from_version": pre_rollback_version, "to_version": to_version,
|
|
1120
|
-
"from_sha": pre_rollback_sha, "to_sha": from_sha}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
def preview_update(project_dir):
|
|
1124
|
-
"""Fetch the target release metadata + scan for breaking-change markers.
|
|
1125
|
-
|
|
1126
|
-
Returns a dict the UI can render before the user confirms the update:
|
|
1127
|
-
{success, latest_version, release_notes, breaking_changes, prerelease,
|
|
1128
|
-
channel, current_version, target_sha}
|
|
1129
|
-
"""
|
|
1130
|
-
try:
|
|
1131
|
-
from src.utils.update_check import check_for_updates # type: ignore
|
|
1132
|
-
except Exception as exc:
|
|
1133
|
-
return {"success": False, "error": f"update_check unavailable: {exc}"}
|
|
1134
|
-
current = _read_current_version(project_dir) or "0.0.0"
|
|
1135
|
-
try:
|
|
1136
|
-
result = check_for_updates(current, project_dir, force=True)
|
|
1137
|
-
except Exception as exc:
|
|
1138
|
-
return {"success": False, "error": str(exc)}
|
|
1139
|
-
return {
|
|
1140
|
-
"success": True,
|
|
1141
|
-
"current_version": current,
|
|
1142
|
-
"latest_version": result.get("latest_version"),
|
|
1143
|
-
"release_notes": result.get("release_notes") or "",
|
|
1144
|
-
"breaking_changes": result.get("release_notes_breaking") or [],
|
|
1145
|
-
"prerelease": bool(result.get("prerelease")),
|
|
1146
|
-
"channel": result.get("channel"),
|
|
1147
|
-
"target_sha": result.get("release_target_sha"),
|
|
1148
|
-
"release_url": result.get("release_url"),
|
|
1149
|
-
"status": result.get("status"),
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
def read_update_history(project_dir, limit=20):
|
|
1154
|
-
"""Most-recent-first list of recorded update attempts. For the dashboard."""
|
|
1155
|
-
import json as _json
|
|
1156
|
-
path = os.path.join(project_dir, "logs", "update_history.json")
|
|
1157
|
-
if not os.path.isfile(path):
|
|
1158
|
-
return {"success": True, "entries": []}
|
|
1159
|
-
try:
|
|
1160
|
-
with open(path, "r", encoding="utf-8") as fh:
|
|
1161
|
-
history = _json.load(fh)
|
|
1162
|
-
except (OSError, _json.JSONDecodeError) as exc:
|
|
1163
|
-
return {"success": False, "error": str(exc), "entries": []}
|
|
1164
|
-
entries = (history.get("entries") or [])[-int(limit):]
|
|
1165
|
-
entries.reverse()
|
|
1166
|
-
return {"success": True, "entries": entries}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
def _restart_installer():
|
|
1170
|
-
print(f" {green('Restarting installer with the updated build...')}")
|
|
1171
|
-
os.execv(sys.executable, [sys.executable, *sys.argv])
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
def _print_update_apply_result(result, *, restart_on_change=True):
|
|
1175
|
-
if result.get("success"):
|
|
1176
|
-
if result.get("dry_run"):
|
|
1177
|
-
print(f" MCP Update: {yellow('Dry run')} {dim(result.get('message', ''))}")
|
|
1178
|
-
return
|
|
1179
|
-
print(f" MCP Update: {green('Update command completed')}")
|
|
1180
|
-
message = str(result.get("message") or "").strip()
|
|
1181
|
-
if message:
|
|
1182
|
-
for line in message.splitlines()[:6]:
|
|
1183
|
-
print(f" {dim(line)}")
|
|
1184
|
-
if result.get("changed") and restart_on_change:
|
|
1185
|
-
_restart_installer()
|
|
1186
|
-
return
|
|
1187
|
-
|
|
1188
|
-
print(f" MCP Update: {yellow('Not applied')} {dim(result.get('message', ''))}")
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
def maybe_prompt_for_update(project_dir, result, *, interactive, force_update=False, dry_run=False):
|
|
1192
|
-
"""Prompt or auto-apply updates only in this human-facing installer."""
|
|
1193
|
-
if not result or result.get("status") != "update_available":
|
|
1194
|
-
return
|
|
1195
|
-
|
|
1196
|
-
decision = update_prompt_decision(result)
|
|
1197
|
-
if force_update or decision.get("action") == "auto":
|
|
1198
|
-
print(f" MCP Update: {yellow('Applying safe fast-forward update...')}")
|
|
1199
|
-
_print_update_apply_result(apply_safe_self_update(project_dir, dry_run=dry_run))
|
|
1200
|
-
return
|
|
1201
|
-
|
|
1202
|
-
if decision.get("action") == "notify":
|
|
1203
|
-
print(f" {dim('Update policy is notify-only; continuing with the current build.')}")
|
|
1204
|
-
return
|
|
1205
|
-
if decision.get("reason") == "ignored":
|
|
1206
|
-
print(f" {dim('This release was ignored; continuing with the current build.')}")
|
|
1207
|
-
return
|
|
1208
|
-
if decision.get("reason") == "snoozed":
|
|
1209
|
-
snooze_note = f"Update reminder snoozed until {decision.get('snooze_until_iso')}."
|
|
1210
|
-
print(f" {dim(snooze_note)}")
|
|
1211
|
-
return
|
|
1212
|
-
if not interactive:
|
|
1213
|
-
return
|
|
1214
|
-
|
|
1215
|
-
latest = result.get("latest_version") or result.get("latest_tag") or "latest"
|
|
1216
|
-
print()
|
|
1217
|
-
print(f" {yellow('A newer DaVinci Resolve MCP is available.')} {dim(f'v{VERSION} -> v{latest}')}")
|
|
1218
|
-
print(f" {dim('Safe auto-update only runs for clean git checkouts with a configured upstream.')}")
|
|
1219
|
-
print(f" {cyan('1')}. Update now")
|
|
1220
|
-
print(f" {cyan('2')}. Continue current build")
|
|
1221
|
-
print(f" {cyan('3')}. Remind me tomorrow")
|
|
1222
|
-
print(f" {cyan('4')}. Ignore this version")
|
|
1223
|
-
print(f" {cyan('5')}. Auto-update this checkout when safe")
|
|
1224
|
-
print(f" {cyan('6')}. Never check automatically")
|
|
1225
|
-
try:
|
|
1226
|
-
choice = input(f" Select {dim('[2]')} ").strip().lower()
|
|
1227
|
-
except (EOFError, KeyboardInterrupt):
|
|
1228
|
-
print()
|
|
1229
|
-
return
|
|
1230
|
-
|
|
1231
|
-
if choice in ("1", "u", "update", "update now"):
|
|
1232
|
-
_print_update_apply_result(apply_safe_self_update(project_dir, dry_run=dry_run))
|
|
1233
|
-
elif choice in ("3", "r", "remind", "later", "snooze"):
|
|
1234
|
-
snooze_update_prompt(project_dir)
|
|
1235
|
-
print(f" MCP Update: {dim('Reminder snoozed for 24 hours.')}")
|
|
1236
|
-
elif choice in ("4", "i", "ignore"):
|
|
1237
|
-
ignore_update_version(project_dir, result)
|
|
1238
|
-
print(f" MCP Update: {dim(f'Ignored v{latest}. Newer releases will still prompt.')}")
|
|
1239
|
-
elif choice in ("5", "a", "auto", "auto-update", "autoupdate"):
|
|
1240
|
-
set_update_mode(project_dir, "auto")
|
|
1241
|
-
print(f" MCP Update: {green('Safe auto-update enabled for this checkout.')}")
|
|
1242
|
-
_print_update_apply_result(apply_safe_self_update(project_dir, dry_run=dry_run))
|
|
1243
|
-
elif choice in ("6", "n", "never", "disable", "disabled"):
|
|
1244
|
-
set_update_mode(project_dir, "never")
|
|
1245
|
-
print(f" MCP Update: {dim('Automatic update checks disabled for this checkout.')}")
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
def prompt_yes_no(question, default=True):
|
|
1249
|
-
"""Prompt for yes/no with a default."""
|
|
1250
|
-
suffix = "[Y/n]" if default else "[y/N]"
|
|
1251
|
-
try:
|
|
1252
|
-
answer = input(f" {question} {dim(suffix)} ").strip().lower()
|
|
1253
|
-
except (EOFError, KeyboardInterrupt):
|
|
1254
|
-
print()
|
|
1255
|
-
return default
|
|
1256
|
-
if not answer:
|
|
1257
|
-
return default
|
|
1258
|
-
return answer in ("y", "yes")
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
def prompt_clients():
|
|
1262
|
-
"""Interactive client selection menu."""
|
|
1263
|
-
print(f"\n Which MCP client(s) do you want to configure?\n")
|
|
1264
|
-
|
|
1265
|
-
for i, client in enumerate(MCP_CLIENTS, 1):
|
|
1266
|
-
path = client["get_path"]()
|
|
1267
|
-
available = path is not None
|
|
1268
|
-
status = ""
|
|
1269
|
-
if not available:
|
|
1270
|
-
status = dim(f" (not available on {platform_name()})")
|
|
1271
|
-
elif path and path.exists():
|
|
1272
|
-
status = green(" (config exists)")
|
|
1273
|
-
|
|
1274
|
-
num = f"{i:>2}"
|
|
1275
|
-
print(f" {cyan(num)}. {client['name']:<28} {dim(client['notes'])}{status}")
|
|
1276
|
-
|
|
1277
|
-
all_num = str(len(MCP_CLIENTS) + 1).rjust(2)
|
|
1278
|
-
manual_num = str(len(MCP_CLIENTS) + 2).rjust(2)
|
|
1279
|
-
print(f"\n {cyan(all_num)}. {bold('All of the above')}")
|
|
1280
|
-
print(f" {cyan(manual_num)}. {bold('Manual setup')} {dim('(print config, I will set it up myself)')}")
|
|
1281
|
-
print(f" {cyan(' 0')}. {dim('Skip client configuration')}")
|
|
1282
|
-
|
|
1283
|
-
print()
|
|
1284
|
-
try:
|
|
1285
|
-
choice = input(f" Select (comma-separated, e.g. 1,3,5): ").strip()
|
|
1286
|
-
except (EOFError, KeyboardInterrupt):
|
|
1287
|
-
print()
|
|
1288
|
-
return []
|
|
1289
|
-
|
|
1290
|
-
if not choice or choice == "0":
|
|
1291
|
-
return []
|
|
1292
|
-
|
|
1293
|
-
selections = []
|
|
1294
|
-
for part in choice.replace(" ", "").split(","):
|
|
1295
|
-
try:
|
|
1296
|
-
idx = int(part)
|
|
1297
|
-
if idx == len(MCP_CLIENTS) + 1: # "All"
|
|
1298
|
-
return CLIENT_IDS + ["manual"]
|
|
1299
|
-
elif idx == len(MCP_CLIENTS) + 2: # "Manual"
|
|
1300
|
-
selections.append("manual")
|
|
1301
|
-
elif 1 <= idx <= len(MCP_CLIENTS):
|
|
1302
|
-
selections.append(MCP_CLIENTS[idx - 1]["id"])
|
|
1303
|
-
except ValueError:
|
|
1304
|
-
# Try matching by name/id
|
|
1305
|
-
part_lower = part.lower()
|
|
1306
|
-
for client in MCP_CLIENTS:
|
|
1307
|
-
if part_lower in client["id"] or part_lower in client["name"].lower():
|
|
1308
|
-
selections.append(client["id"])
|
|
1309
|
-
break
|
|
1310
|
-
|
|
1311
|
-
return list(dict.fromkeys(selections)) # deduplicate, preserve order
|
|
1312
|
-
|
|
1313
|
-
# ─── Main Install Flow ───────────────────────────────────────────────────────
|
|
1314
|
-
|
|
1315
|
-
def main():
|
|
1316
|
-
parser = argparse.ArgumentParser(
|
|
1317
|
-
description="DaVinci Resolve MCP Server — Universal Installer",
|
|
1318
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1319
|
-
epilog=textwrap.dedent("""\
|
|
1320
|
-
Examples:
|
|
1321
|
-
python install.py Interactive mode
|
|
1322
|
-
python install.py --clients all Configure all clients
|
|
1323
|
-
python install.py --clients cursor,claude-desktop
|
|
1324
|
-
python install.py --clients manual Just print the config
|
|
1325
|
-
python install.py --no-venv --clients cursor Skip venv, configure Cursor
|
|
1326
|
-
python install.py --dry-run --clients all Preview without writing
|
|
1327
|
-
python install.py --update-policy auto Enable guarded auto-updates
|
|
1328
|
-
python install.py --update-policy never Disable update checks
|
|
1329
|
-
""")
|
|
1330
|
-
)
|
|
1331
|
-
parser.add_argument(
|
|
1332
|
-
"--clients", type=str, default=None,
|
|
1333
|
-
help="Comma-separated client IDs, or 'all' / 'manual' (skip interactive prompt)"
|
|
1334
|
-
)
|
|
1335
|
-
parser.add_argument(
|
|
1336
|
-
"--no-venv", action="store_true",
|
|
1337
|
-
help="Skip virtual environment creation (use system Python)"
|
|
1338
|
-
)
|
|
1339
|
-
parser.add_argument(
|
|
1340
|
-
"--dry-run", action="store_true",
|
|
1341
|
-
help="Preview config changes without writing files"
|
|
1342
|
-
)
|
|
1343
|
-
parser.add_argument(
|
|
1344
|
-
"--python", type=str, default=None,
|
|
1345
|
-
help="Path to Python executable to use in MCP configs"
|
|
1346
|
-
)
|
|
1347
|
-
parser.add_argument(
|
|
1348
|
-
"--server", type=str, default=None,
|
|
1349
|
-
help="Path to the MCP server script"
|
|
1350
|
-
)
|
|
1351
|
-
parser.add_argument(
|
|
1352
|
-
"--update-policy",
|
|
1353
|
-
choices=["prompt", "auto", "notify", "never"],
|
|
1354
|
-
default=None,
|
|
1355
|
-
help="Set local update policy: prompt, auto, notify, or never"
|
|
1356
|
-
)
|
|
1357
|
-
parser.add_argument(
|
|
1358
|
-
"--update-now", action="store_true",
|
|
1359
|
-
help="Apply a safe git fast-forward update if a newer release is available"
|
|
1360
|
-
)
|
|
1361
|
-
parser.add_argument(
|
|
1362
|
-
"--clear-update-preferences", action="store_true",
|
|
1363
|
-
help="Clear ignored-version and snooze update preferences"
|
|
1364
|
-
)
|
|
1365
|
-
|
|
1366
|
-
args = parser.parse_args()
|
|
1367
|
-
interactive = args.clients is None
|
|
1368
|
-
|
|
1369
|
-
# ── Banner ──
|
|
1370
|
-
if interactive:
|
|
1371
|
-
print_banner()
|
|
1372
|
-
|
|
1373
|
-
project_dir = Path(__file__).resolve().parent
|
|
1374
|
-
total_steps = 5
|
|
1375
|
-
|
|
1376
|
-
if args.clear_update_preferences:
|
|
1377
|
-
clear_update_prompt_preferences(project_dir)
|
|
1378
|
-
print(f" MCP Update: {green('Cleared ignored-version and snooze preferences')}")
|
|
1379
|
-
if args.update_policy:
|
|
1380
|
-
set_update_mode(project_dir, args.update_policy)
|
|
1381
|
-
print(f" MCP Update: {green('Policy set')} {dim(args.update_policy)}")
|
|
1382
|
-
|
|
1383
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1384
|
-
# STEP 1: Platform & Resolve Detection
|
|
1385
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1386
|
-
|
|
1387
|
-
if interactive:
|
|
1388
|
-
print_step(1, total_steps, "Detecting Platform & DaVinci Resolve")
|
|
1389
|
-
|
|
1390
|
-
print(f" Platform: {bold(platform_name())} ({platform.machine()})")
|
|
1391
|
-
update_result = print_update_status(project_dir, force=args.update_now)
|
|
1392
|
-
maybe_prompt_for_update(
|
|
1393
|
-
project_dir,
|
|
1394
|
-
update_result,
|
|
1395
|
-
interactive=interactive,
|
|
1396
|
-
force_update=args.update_now,
|
|
1397
|
-
dry_run=args.dry_run,
|
|
1398
|
-
)
|
|
1399
|
-
|
|
1400
|
-
api_path, lib_path = find_resolve_paths()
|
|
1401
|
-
|
|
1402
|
-
if api_path:
|
|
1403
|
-
print(f" API Path: {green(api_path)}")
|
|
1404
|
-
else:
|
|
1405
|
-
print(f" API Path: {red('Not found')}")
|
|
1406
|
-
if is_linux():
|
|
1407
|
-
print(f" {dim('Tip: DaVinci Resolve typically installs to /opt/resolve/')}")
|
|
1408
|
-
print(f" {dim(' Set RESOLVE_SCRIPT_API environment variable if installed elsewhere')}")
|
|
1409
|
-
|
|
1410
|
-
# Check environment variable fallback
|
|
1411
|
-
env_api = os.environ.get("RESOLVE_SCRIPT_API")
|
|
1412
|
-
if env_api and os.path.isdir(env_api):
|
|
1413
|
-
api_path = env_api
|
|
1414
|
-
print(f" {green('Found via $RESOLVE_SCRIPT_API:')} {api_path}")
|
|
1415
|
-
|
|
1416
|
-
if lib_path:
|
|
1417
|
-
print(f" Library: {green(lib_path)}")
|
|
1418
|
-
else:
|
|
1419
|
-
print(f" Library: {yellow('Not found')} {dim('(optional — API path is sufficient)')}")
|
|
1420
|
-
|
|
1421
|
-
resolve_running = check_resolve_running()
|
|
1422
|
-
if resolve_running:
|
|
1423
|
-
print(f" Resolve: {green('Running')}")
|
|
1424
|
-
else:
|
|
1425
|
-
print(f" Resolve: {yellow('Not running')} {dim('(start Resolve to verify connection)')}")
|
|
1426
|
-
|
|
1427
|
-
if not api_path:
|
|
1428
|
-
print(f"\n {yellow('Warning:')} Could not auto-detect DaVinci Resolve installation.")
|
|
1429
|
-
print(f" The installer will continue, but you may need to set RESOLVE_SCRIPT_API manually.")
|
|
1430
|
-
if interactive and not prompt_yes_no("Continue anyway?"):
|
|
1431
|
-
print(f"\n {dim('Aborted.')}")
|
|
1432
|
-
sys.exit(1)
|
|
1433
|
-
|
|
1434
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1435
|
-
# STEP 2: Python Virtual Environment
|
|
1436
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1437
|
-
|
|
1438
|
-
venv_path = project_dir / "venv"
|
|
1439
|
-
venv_python = get_venv_python(venv_path)
|
|
1440
|
-
skip_venv = args.no_venv
|
|
1441
|
-
|
|
1442
|
-
if interactive:
|
|
1443
|
-
print_step(2, total_steps, "Python Environment")
|
|
1444
|
-
|
|
1445
|
-
if skip_venv:
|
|
1446
|
-
print(f" Skipping venv (--no-venv)")
|
|
1447
|
-
python_path = Path(args.python) if args.python else Path(sys.executable)
|
|
1448
|
-
require_supported_python(python_path, "Selected Python")
|
|
1449
|
-
print(f" Using: {python_path}")
|
|
1450
|
-
elif venv_path.exists() and venv_python.exists():
|
|
1451
|
-
print(f" Venv: {green('Already exists')} at {dim(str(venv_path))}")
|
|
1452
|
-
python_path = venv_python
|
|
1453
|
-
require_supported_python(python_path, "Existing venv")
|
|
1454
|
-
|
|
1455
|
-
# Check if deps are installed
|
|
1456
|
-
try:
|
|
1457
|
-
result = subprocess.run(
|
|
1458
|
-
[str(python_path), "-c", "import mcp; print('ok')"],
|
|
1459
|
-
capture_output=True, text=True
|
|
1460
|
-
)
|
|
1461
|
-
if result.stdout.strip() == "ok":
|
|
1462
|
-
print(f" MCP SDK: {green('Installed')}")
|
|
1463
|
-
else:
|
|
1464
|
-
print(f" MCP SDK: {yellow('Missing')} — installing...")
|
|
1465
|
-
install_dependencies(venv_path, project_dir)
|
|
1466
|
-
print(f" MCP SDK: {green('Installed')}")
|
|
1467
|
-
except Exception:
|
|
1468
|
-
install_dependencies(venv_path, project_dir)
|
|
1469
|
-
print(f" MCP SDK: {green('Installed')}")
|
|
1470
|
-
else:
|
|
1471
|
-
if interactive:
|
|
1472
|
-
create_venv_ok = prompt_yes_no("Create virtual environment?")
|
|
1473
|
-
else:
|
|
1474
|
-
create_venv_ok = True
|
|
1475
|
-
|
|
1476
|
-
if create_venv_ok:
|
|
1477
|
-
require_current_python("Virtual environment")
|
|
1478
|
-
create_venv(venv_path)
|
|
1479
|
-
python_path = venv_python
|
|
1480
|
-
require_supported_python(python_path, "Created venv")
|
|
1481
|
-
install_dependencies(venv_path, project_dir)
|
|
1482
|
-
print(f" Venv: {green('Created')}")
|
|
1483
|
-
print(f" MCP SDK: {green('Installed')}")
|
|
1484
|
-
else:
|
|
1485
|
-
python_path = Path(args.python) if args.python else Path(sys.executable)
|
|
1486
|
-
require_supported_python(python_path, "Selected Python")
|
|
1487
|
-
print(f" Using system Python: {python_path}")
|
|
1488
|
-
|
|
1489
|
-
# Override python path if explicitly provided
|
|
1490
|
-
if args.python:
|
|
1491
|
-
python_path = Path(args.python)
|
|
1492
|
-
require_supported_python(python_path, "Selected --python")
|
|
1493
|
-
|
|
1494
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1495
|
-
# STEP 3: Locate Server Script
|
|
1496
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1497
|
-
|
|
1498
|
-
if interactive:
|
|
1499
|
-
print_step(3, total_steps, "Server Script")
|
|
1500
|
-
|
|
1501
|
-
if args.server:
|
|
1502
|
-
server_path = Path(args.server).resolve()
|
|
1503
|
-
else:
|
|
1504
|
-
# Try to find the compound server first.
|
|
1505
|
-
candidates = [
|
|
1506
|
-
project_dir / "src" / "server.py",
|
|
1507
|
-
project_dir / "src" / "resolve_mcp_server.py",
|
|
1508
|
-
project_dir / "src" / "main.py",
|
|
1509
|
-
project_dir / "resolve_mcp_server.py",
|
|
1510
|
-
]
|
|
1511
|
-
server_path = None
|
|
1512
|
-
for c in candidates:
|
|
1513
|
-
if c.exists():
|
|
1514
|
-
server_path = c
|
|
1515
|
-
break
|
|
1516
|
-
if server_path is None:
|
|
1517
|
-
server_path = candidates[0] # default even if not found yet
|
|
1518
|
-
|
|
1519
|
-
if server_path.exists():
|
|
1520
|
-
print(f" Server: {green(str(server_path))}")
|
|
1521
|
-
else:
|
|
1522
|
-
print(f" Server: {yellow(str(server_path))} {dim('(file not found — check path)')}")
|
|
1523
|
-
|
|
1524
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1525
|
-
# STEP 4: Configure MCP Clients
|
|
1526
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1527
|
-
|
|
1528
|
-
if interactive:
|
|
1529
|
-
print_step(4, total_steps, "MCP Client Configuration")
|
|
1530
|
-
|
|
1531
|
-
# Determine which clients to configure
|
|
1532
|
-
if args.clients:
|
|
1533
|
-
if args.clients.lower() == "all":
|
|
1534
|
-
selected_ids = CLIENT_IDS
|
|
1535
|
-
elif args.clients.lower() == "manual":
|
|
1536
|
-
selected_ids = ["manual"]
|
|
1537
|
-
else:
|
|
1538
|
-
selected_ids = [s.strip() for s in args.clients.split(",")]
|
|
1539
|
-
elif interactive:
|
|
1540
|
-
selected_ids = prompt_clients()
|
|
1541
|
-
else:
|
|
1542
|
-
selected_ids = []
|
|
1543
|
-
|
|
1544
|
-
# Show manual config if requested
|
|
1545
|
-
show_manual = "manual" in selected_ids
|
|
1546
|
-
client_ids = [c for c in selected_ids if c != "manual"]
|
|
1547
|
-
|
|
1548
|
-
configured = []
|
|
1549
|
-
skipped = []
|
|
1550
|
-
|
|
1551
|
-
for client_id in client_ids:
|
|
1552
|
-
client = next((c for c in MCP_CLIENTS if c["id"] == client_id), None)
|
|
1553
|
-
if not client:
|
|
1554
|
-
print(f" {yellow('Unknown client:')} {client_id}")
|
|
1555
|
-
skipped.append(client_id)
|
|
1556
|
-
continue
|
|
1557
|
-
|
|
1558
|
-
success, message = write_client_config(
|
|
1559
|
-
client, python_path, server_path, api_path, lib_path, dry_run=args.dry_run
|
|
1560
|
-
)
|
|
1561
|
-
|
|
1562
|
-
if success:
|
|
1563
|
-
if args.dry_run:
|
|
1564
|
-
print(f"\n {cyan(client['name'])} {dim('(dry run)')}")
|
|
1565
|
-
for line in message.split("\n"):
|
|
1566
|
-
print(f" {line}")
|
|
1567
|
-
configured.append(client["name"])
|
|
1568
|
-
else:
|
|
1569
|
-
print(f" {green('✓')} {client['name']:<28} → {dim(message)}")
|
|
1570
|
-
configured.append(client["name"])
|
|
1571
|
-
else:
|
|
1572
|
-
print(f" {yellow('⊘')} {client['name']:<28} {dim(message)}")
|
|
1573
|
-
skipped.append(client["name"])
|
|
1574
|
-
|
|
1575
|
-
# Show manual config
|
|
1576
|
-
if show_manual:
|
|
1577
|
-
standard, vscode_fmt, zed_fmt, opencode_fmt = generate_manual_config(
|
|
1578
|
-
python_path, server_path, api_path, lib_path
|
|
1579
|
-
)
|
|
1580
|
-
env_preview = build_server_env(python_path, api_path, lib_path)
|
|
1581
|
-
print(f"\n {bold('Manual Configuration')}")
|
|
1582
|
-
print(f" {'─' * 50}")
|
|
1583
|
-
print(f"\n {cyan('Standard format')} (Claude Desktop, Cursor, Windsurf, Cline, Roo Code, Continue):")
|
|
1584
|
-
print()
|
|
1585
|
-
for line in standard.split("\n"):
|
|
1586
|
-
print(f" {line}")
|
|
1587
|
-
print(f"\n {cyan('VS Code format')} (GitHub Copilot agent mode — save as .vscode/mcp.json):")
|
|
1588
|
-
print()
|
|
1589
|
-
for line in vscode_fmt.split("\n"):
|
|
1590
|
-
print(f" {line}")
|
|
1591
|
-
print(f"\n {cyan('Zed format')} (add to ~/.config/zed/settings.json):")
|
|
1592
|
-
print()
|
|
1593
|
-
for line in zed_fmt.split("\n"):
|
|
1594
|
-
print(f" {line}")
|
|
1595
|
-
print(f"\n {cyan('OpenCode format')} (add to ~/.config/opencode/opencode.json or a project opencode.json):")
|
|
1596
|
-
print()
|
|
1597
|
-
for line in opencode_fmt.split("\n"):
|
|
1598
|
-
print(f" {line}")
|
|
1599
|
-
print(f"\n {cyan('JetBrains IDEs')} (IntelliJ, WebStorm, PyCharm, etc.):")
|
|
1600
|
-
print(f" Settings → Tools → AI Assistant → Model Context Protocol (MCP)")
|
|
1601
|
-
print(f" Add server with command: {python_path} {server_path}")
|
|
1602
|
-
for key, value in env_preview.items():
|
|
1603
|
-
print(f" Set env: {key}={value}")
|
|
1604
|
-
print()
|
|
1605
|
-
|
|
1606
|
-
if not selected_ids:
|
|
1607
|
-
print(f" {dim('No clients selected — skipping configuration')}")
|
|
1608
|
-
|
|
1609
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1610
|
-
# STEP 5: Verify Connection
|
|
1611
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1612
|
-
|
|
1613
|
-
if interactive:
|
|
1614
|
-
print_step(5, total_steps, "Verification")
|
|
1615
|
-
|
|
1616
|
-
if api_path:
|
|
1617
|
-
success, message = verify_resolve_connection(python_path, api_path, lib_path)
|
|
1618
|
-
try:
|
|
1619
|
-
py_abi_risk = is_abi_risk_python_version(_version_for_python(python_path))
|
|
1620
|
-
except Exception:
|
|
1621
|
-
py_abi_risk = False
|
|
1622
|
-
if success:
|
|
1623
|
-
if "not running" in message.lower():
|
|
1624
|
-
print(f" API: {green('Module loads OK')}")
|
|
1625
|
-
# If Resolve IS running but the bridge still reported no connection
|
|
1626
|
-
# on a 3.13+ interpreter, that is the ABI-mismatch signature.
|
|
1627
|
-
if resolve_running and py_abi_risk:
|
|
1628
|
-
print(
|
|
1629
|
-
f" Resolve: {yellow('Running, but the scripting bridge returned no connection')}"
|
|
1630
|
-
)
|
|
1631
|
-
print(
|
|
1632
|
-
f" This can happen on Python 3.13+ with older Resolve builds. "
|
|
1633
|
-
f"If MCP tools fail, recreate the venv with Python 3.10-3.12."
|
|
1634
|
-
)
|
|
1635
|
-
else:
|
|
1636
|
-
print(f" Resolve: {yellow('Not running')} — start Resolve to use MCP tools")
|
|
1637
|
-
else:
|
|
1638
|
-
print(f" Connected: {green(message)}")
|
|
1639
|
-
else:
|
|
1640
|
-
print(f" Verify: {yellow(message)}")
|
|
1641
|
-
if py_abi_risk:
|
|
1642
|
-
print(
|
|
1643
|
-
f" On Python 3.13+ this may be an ABI mismatch with Resolve's "
|
|
1644
|
-
f"scripting library — try Python 3.10-3.12 if it persists."
|
|
1645
|
-
)
|
|
1646
|
-
else:
|
|
1647
|
-
print(f" {yellow('Skipped')} — Resolve API path not detected")
|
|
1648
|
-
|
|
1649
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1650
|
-
# Summary
|
|
1651
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
1652
|
-
|
|
1653
|
-
print(f"\n {'═' * 50}")
|
|
1654
|
-
if configured or show_manual:
|
|
1655
|
-
print(f" {green(bold('Setup complete!'))}")
|
|
1656
|
-
if configured:
|
|
1657
|
-
print(f" Configured: {', '.join(configured)}")
|
|
1658
|
-
print()
|
|
1659
|
-
print(f" {bold('Next steps:')}")
|
|
1660
|
-
if not resolve_running:
|
|
1661
|
-
print(f" 1. Start DaVinci Resolve")
|
|
1662
|
-
print(f" 2. Open your MCP client")
|
|
1663
|
-
print(f" 3. Start using natural language to control Resolve!")
|
|
1664
|
-
else:
|
|
1665
|
-
print(f" 1. Open your MCP client")
|
|
1666
|
-
print(f" 2. Start using natural language to control Resolve!")
|
|
1667
|
-
print()
|
|
1668
|
-
print(f" {dim(f'Server: {server_path}')}")
|
|
1669
|
-
print(f" {dim(f'Python: {python_path}')}")
|
|
1670
|
-
if api_path:
|
|
1671
|
-
print(f" {dim(f'API: {api_path}')}")
|
|
1672
|
-
elif not selected_ids:
|
|
1673
|
-
print(f" {green(bold('Environment ready!'))}")
|
|
1674
|
-
print(f" Run {cyan('python install.py --clients all')} to configure MCP clients later.")
|
|
1675
|
-
else:
|
|
1676
|
-
print(f" {yellow('No clients configured.')}")
|
|
1677
|
-
print(f" Run {cyan('python install.py')} again to retry.")
|
|
1678
|
-
|
|
1679
|
-
print()
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
if __name__ == "__main__":
|
|
1683
|
-
try:
|
|
1684
|
-
main()
|
|
1685
|
-
except KeyboardInterrupt:
|
|
1686
|
-
print(f"\n\n {dim('Interrupted.')}\n")
|
|
1687
|
-
sys.exit(1)
|