@ngocsangairvds/vsaf 3.1.27 → 3.2.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/package.json +2 -2
- package/src/global.js +70 -10
- package/tools/skills/vds-scripts-skill/.openskills.json +6 -0
- package/tools/skills/vds-scripts-skill/QUALITY.md +44 -0
- package/tools/skills/vds-scripts-skill/SKILL.md +135 -0
- package/tools/skills/vds-scripts-skill/references/audit-commands.md +171 -0
- package/tools/skills/vds-scripts-skill/references/capability-index.md +34 -0
- package/tools/skills/vds-scripts-skill/references/development-commands.md +12 -0
- package/tools/skills/vds-scripts-skill/references/google-sheets.md +73 -0
- package/tools/skills/vds-scripts-skill/references/integration-commands.md +17 -0
- package/tools/skills/vds-scripts-skill/references/platform-bootstrap.md +31 -0
- package/tools/skills/vds-scripts-skill/references/specialist-routing.md +14 -0
- package/tools/skills/vds-scripts-skill/references/validation-commands.md +15 -0
- package/tools/skills/vsaf-build/SKILL.md +32 -2
- package/tools/skills/vsaf-ship/SKILL.md +41 -10
- package/tools/skills/vsaf-test/SKILL.md +8 -0
- package/tools/vds-scripts/.mcp.json +11 -0
- package/tools/vds-scripts/.secrets.baseline +133 -0
- package/tools/vds-scripts/AGENTS.md +152 -0
- package/tools/vds-scripts/CLAUDE.md +101 -0
- package/tools/vds-scripts/CLI_COMMAND_OPTIMIZATION.md +156 -0
- package/tools/vds-scripts/PACKAGE_P125B_IMPLEMENTATION_SUMMARY.md +131 -0
- package/tools/vds-scripts/PROJECT_COMPLETION_SUMMARY.md +45 -0
- package/tools/vds-scripts/README.md +97 -0
- package/tools/vds-scripts/bitbucket_manifest_mapping.toml +34 -0
- package/tools/vds-scripts/bitbucket_orchestrator/ARCHITECTURE_ANALYSIS.md +258 -0
- package/tools/vds-scripts/bitbucket_orchestrator/BITBUCKET_API_PRACTICES.md +393 -0
- package/tools/vds-scripts/bitbucket_orchestrator/EVALUATION_REPORT.md +61 -0
- package/tools/vds-scripts/bitbucket_orchestrator/FEATURES.md +908 -0
- package/tools/vds-scripts/bitbucket_orchestrator/README.md +687 -0
- package/tools/vds-scripts/bitbucket_orchestrator/pyproject.toml +40 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/__init__.py +20 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/async_client.py +657 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/cli.py +2108 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/client.py +2534 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/config.py +171 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/errors.py +67 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/factory.py +185 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/protocols.py +244 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/__init__.py +8 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/conftest.py +65 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_advanced_search.py +151 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_async_client.py +546 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_branch_permissions.py +145 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_cli.py +115 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client.py +157 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_branch_conditions.py +79 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_advanced.py +163 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_file.py +32 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_deployment_environments.py +194 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_issues.py +164 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pipelines_advanced.py +179 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pr_blockers.py +119 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_repository_variables.py +156 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code.py +98 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_advanced.py +282 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_insights.py +335 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_conditions.py +147 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_config.py +131 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_deployment_env.py +352 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_factory.py +371 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_fork_operations.py +204 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_issue_cli.py +261 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pipeline_advanced.py +270 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pr_blocker.py +204 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_protocols.py +334 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_settings.py +343 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_variables.py +270 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_webhooks.py +189 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_workspace.py +233 -0
- package/tools/vds-scripts/bitbucket_orchestrator/uv.lock +742 -0
- package/tools/vds-scripts/confluence_orchestrator/Dockerfile +19 -0
- package/tools/vds-scripts/confluence_orchestrator/README.md +412 -0
- package/tools/vds-scripts/confluence_orchestrator/SYNC_SCRIPTS.md +127 -0
- package/tools/vds-scripts/confluence_orchestrator/SYNC_STANDARDIZATION.md +108 -0
- package/tools/vds-scripts/confluence_orchestrator/pyproject.toml +48 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/__init__.py +20 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/cli.py +2532 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/config.py +175 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content.py +290 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content_v2.py +94 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/crawl_tree.py +1835 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/errors.py +80 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/eventing.py +109 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/http.py +1114 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/orchestration.py +165 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/reporting.py +78 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/tree.py +121 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_from_markdown.py +213 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_to_confluence.py +305 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_png_attachments.py +305 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/__init__.py +0 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/conftest.py +8 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_content.py +224 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_search.py +188 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_cache_management.py +247 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_cli.py +499 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_config.py +83 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_content.py +186 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_content_flags.py +27 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_crawl_tree.py +2250 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_draft_management.py +223 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing.py +71 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_chaos.py +37 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_rate_limit.py +44 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_timeout.py +49 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_export.py +230 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_history.py +204 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_http.py +117 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_orchestration.py +91 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_reporting.py +24 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_search_cql.py +34 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_space_management.py +237 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_space_permissions.py +332 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_user_group_management.py +388 -0
- package/tools/vds-scripts/confluence_orchestrator/uv.lock +1023 -0
- package/tools/vds-scripts/git_orchestrator/ENHANCEMENT_SUMMARY.md +119 -0
- package/tools/vds-scripts/git_orchestrator/README.md +280 -0
- package/tools/vds-scripts/git_orchestrator/VERIFICATION_REPORT.md +152 -0
- package/tools/vds-scripts/git_orchestrator/pyproject.toml +35 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__init__.py +7 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__main__.py +4 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/cli.py +847 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/logging_config.py +63 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/manifest.py +129 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/orchestrator.py +819 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/reporting.py +53 -0
- package/tools/vds-scripts/git_orchestrator/tests/__init__.py +0 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_cli_settings.py +21 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_integration.py +74 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_manifest.py +79 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_orchestrator.py +204 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_public_api.py +236 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_resilience.py +345 -0
- package/tools/vds-scripts/git_orchestrator/uv.lock +271 -0
- package/tools/vds-scripts/jira_orchestrator/README.md +770 -0
- package/tools/vds-scripts/jira_orchestrator/pyproject.toml +39 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/__init__.py +1 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/adapter.py +1320 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/cli.py +2271 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/config.py +138 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/errors.py +67 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/reporting.py +65 -0
- package/tools/vds-scripts/jira_orchestrator/tests/__init__.py +1 -0
- package/tools/vds-scripts/jira_orchestrator/tests/conftest.py +86 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_agile_list_payloads.py +54 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_bulk_operations.py +69 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_components.py +57 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_createmeta.py +45 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_dashboard.py +117 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_issue_properties.py +54 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_permissions_compat.py +42 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_reindex.py +42 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_remote_links.py +76 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_transitions.py +91 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_user_management.py +110 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_version_management.py +133 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_watchers.py +41 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_advanced_search.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_agile.py +256 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_application_properties.py +193 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_backlog.py +91 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_bulk_operations.py +277 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_cli.py +106 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_components.py +106 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_config.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_dashboard.py +122 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_discover_fields.py +207 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_filter_management.py +333 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_archiving.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_links.py +257 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_properties.py +171 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_link_types.py +314 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_parse_set.py +37 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_permissions.py +273 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_reindex.py +81 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_remote_links.py +254 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_security_schemes.py +170 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_transitions_changelog.py +114 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_user_management.py +226 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_version_management.py +339 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_watchers.py +101 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_worklog.py +223 -0
- package/tools/vds-scripts/jira_orchestrator/uv.lock +738 -0
- package/tools/vds-scripts/mcp_server/Dockerfile +34 -0
- package/tools/vds-scripts/mcp_server/README.md +140 -0
- package/tools/vds-scripts/mcp_server/pyproject.toml +42 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/__init__.py +4 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/config.py +36 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/server.py +66 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/__init__.py +14 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/bitbucket_tools.py +47 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/confluence_tools.py +59 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/git_tools.py +71 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/jira_tools.py +63 -0
- package/tools/vds-scripts/mcp_server/tests/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/conftest.py +29 -0
- package/tools/vds-scripts/mcp_server/tests/unit/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_bitbucket_tools.py +25 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_confluence_tools.py +25 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_git_tools.py +32 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_jira_tools.py +32 -0
- package/tools/vds-scripts/mcp_server/tests/verification/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_confluence_tools.py +40 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_jira_tools.py +37 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_tool_registration.py +47 -0
- package/tools/vds-scripts/mcp_server/uv.lock +1032 -0
- package/tools/vds-scripts/mypy.ini +5 -0
- package/tools/vds-scripts/pyproject.toml +29 -0
- package/tools/vds-scripts/repo-manifest.yaml +273 -0
- package/tools/vds-scripts/repo-manifest.yaml.example +25 -0
- package/tools/vds-scripts/scripts/BRD-Validation-API.postman_collection.json +706 -0
- package/tools/vds-scripts/scripts/BRD-Validation-README.md +308 -0
- package/tools/vds-scripts/scripts/README.md +162 -0
- package/tools/vds-scripts/scripts/bootstrap_uv.sh +30 -0
- package/tools/vds-scripts/scripts/brd-validation-environment.json +51 -0
- package/tools/vds-scripts/scripts/brd-validation-test-results.json +13023 -0
- package/tools/vds-scripts/scripts/brd_coverage_report.json +276 -0
- package/tools/vds-scripts/scripts/create_memory_session.py +35 -0
- package/tools/vds-scripts/scripts/deployment/load_docker_images_offline.sh +90 -0
- package/tools/vds-scripts/scripts/final_completion_report.md +139 -0
- package/tools/vds-scripts/scripts/folder_structure_report.json +321 -0
- package/tools/vds-scripts/scripts/generate_completion_report.py +125 -0
- package/tools/vds-scripts/scripts/generate_intellij_modules.py +150 -0
- package/tools/vds-scripts/scripts/link_integrity_report.json +807 -0
- package/tools/vds-scripts/scripts/move_audit_artifact_pages.py +255 -0
- package/tools/vds-scripts/scripts/move_audit_artifact_pages_rest.py +165 -0
- package/tools/vds-scripts/scripts/move_wrong_dept_pages.py +216 -0
- package/tools/vds-scripts/scripts/save_intellij_memories.py +120 -0
- package/tools/vds-scripts/scripts/save_memories_to_vds_ai.py +83 -0
- package/tools/vds-scripts/scripts/save_memories_vds_style.py +129 -0
- package/tools/vds-scripts/scripts/search_intellij_memories.py +50 -0
- package/tools/vds-scripts/scripts/setup_intellij_workspace.py +65 -0
- package/tools/vds-scripts/scripts/target-state-automation/README.md +89 -0
- package/tools/vds-scripts/scripts/target-state-automation/confluence_sync_coordinator.sh +27 -0
- package/tools/vds-scripts/scripts/target-state-automation/coordination.sh +114 -0
- package/tools/vds-scripts/scripts/target-state-automation/diagram_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/docs_root.sh +22 -0
- package/tools/vds-scripts/scripts/target-state-automation/generate_diagrams.sh +22 -0
- package/tools/vds-scripts/scripts/target-state-automation/markdown_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/progress_dashboard.sh +17 -0
- package/tools/vds-scripts/scripts/target-state-automation/schema_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/sync_confluence.sh +30 -0
- package/tools/vds-scripts/scripts/target-state-automation/update_dependencies.sh +19 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_links.sh +86 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_markdown.sh +52 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_schemas.sh +26 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_structure.sh +98 -0
- package/tools/vds-scripts/scripts/update_modules_xml.py +190 -0
- package/tools/vds-scripts/scripts/uv-workspace-alignment-verification-2026-03-25.md +128 -0
- package/tools/vds-scripts/scripts/validate_brd_coverage.py +179 -0
- package/tools/vds-scripts/scripts/validate_folder_structure.py +240 -0
- package/tools/vds-scripts/scripts/validate_link_integrity.py +272 -0
- package/tools/vds-scripts/scripts/vds_sh_helpers.sh +180 -0
- package/tools/vds-scripts/scripts/verification/phase2_portable_paths_ubuntu_docker.sh +26 -0
- package/tools/vds-scripts/scripts/worktree_uv.sh +48 -0
- package/tools/vds-scripts/uv.lock +8 -0
- package/tools/vds-scripts/vds_cli/README.md +126 -0
- package/tools/vds-scripts/vds_cli/VERIFICATION_REPORT.md +41 -0
- package/tools/vds-scripts/vds_cli/pyproject.toml +38 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/__init__.py +3 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/cli.py +173 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/docs_sync.py +1203 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/env.py +41 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/__init__.py +3 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/google_sheets_orchestrator.py +198 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/router.py +93 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/sync_api.py +647 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/sync_service.py +266 -0
- package/tools/vds-scripts/vds_cli/tests/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/conftest.py +49 -0
- package/tools/vds-scripts/vds_cli/tests/unit/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_cli.py +143 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_docs_sync.py +422 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_env.py +51 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_router.py +72 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_sync_api.py +357 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_sync_service.py +160 -0
- package/tools/vds-scripts/vds_cli/tests/verification/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_bitbucket_real.py +33 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_confluence_real.py +35 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_jira_real.py +41 -0
- package/tools/vds-scripts/vds_cli/uv.lock +524 -0
- package/tools/vds-scripts/vds_cli_common/README.md +190 -0
- package/tools/vds-scripts/vds_cli_common/pyproject.toml +92 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/__init__.py +34 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/completers.py +139 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/context.py +201 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/env.py +119 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/errors.py +318 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/output.py +284 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/paths.py +78 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/testing.py +213 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/version.py +85 -0
- package/tools/vds-scripts/vds_cli_common/tests/__init__.py +1 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_completers.py +148 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_context.py +192 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_env.py +102 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_errors.py +186 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_output.py +229 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_paths.py +61 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_testing.py +138 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_version.py +64 -0
package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/orchestration.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Batch polling and webhook management utilities for Confluence Server/DC."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .http import ConfluenceClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class BatchMetrics:
|
|
16
|
+
"""Execution metrics captured during a batch scan."""
|
|
17
|
+
|
|
18
|
+
scanned: int = 0
|
|
19
|
+
pages_processed: int = 0
|
|
20
|
+
requests: int = 0
|
|
21
|
+
started_at: float = field(default_factory=time.time)
|
|
22
|
+
finished_at: float | None = None
|
|
23
|
+
|
|
24
|
+
def mark_request(self) -> None:
|
|
25
|
+
self.requests += 1
|
|
26
|
+
|
|
27
|
+
def mark_processed(self, count: int) -> None:
|
|
28
|
+
self.pages_processed += count
|
|
29
|
+
|
|
30
|
+
def mark_finished(self) -> None:
|
|
31
|
+
self.finished_at = time.time()
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def duration_seconds(self) -> float | None:
|
|
35
|
+
if self.finished_at is None:
|
|
36
|
+
return None
|
|
37
|
+
return round(self.finished_at - self.started_at, 3)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BatchRunner:
|
|
41
|
+
"""Polling utilities for change detection using REST v1 CQL queries."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
http_client: ConfluenceClient,
|
|
46
|
+
*,
|
|
47
|
+
expand: Sequence[str] | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._http = http_client
|
|
50
|
+
self._expand = expand
|
|
51
|
+
self._last_metrics: BatchMetrics | None = None
|
|
52
|
+
|
|
53
|
+
def scan(
|
|
54
|
+
self,
|
|
55
|
+
cql: str,
|
|
56
|
+
*,
|
|
57
|
+
limit: int = 25,
|
|
58
|
+
max_results: int | None = None,
|
|
59
|
+
start: int = 0,
|
|
60
|
+
expand: Sequence[str] | None = None,
|
|
61
|
+
) -> Iterator[dict[str, Any]]:
|
|
62
|
+
"""Yield individual content results for the given CQL query."""
|
|
63
|
+
|
|
64
|
+
metrics = BatchMetrics()
|
|
65
|
+
self._last_metrics = metrics
|
|
66
|
+
applied_expand = expand if expand is not None else self._expand
|
|
67
|
+
fetched = 0
|
|
68
|
+
current_start = start
|
|
69
|
+
|
|
70
|
+
while True:
|
|
71
|
+
remaining = None if max_results is None else max_results - fetched
|
|
72
|
+
page_limit = limit if remaining is None else min(limit, remaining)
|
|
73
|
+
params = {
|
|
74
|
+
"cql": cql,
|
|
75
|
+
"limit": page_limit,
|
|
76
|
+
"start": current_start,
|
|
77
|
+
}
|
|
78
|
+
if applied_expand:
|
|
79
|
+
params["expand"] = ",".join(applied_expand)
|
|
80
|
+
|
|
81
|
+
metrics.mark_request()
|
|
82
|
+
# Use SDK's cql method instead of raw request
|
|
83
|
+
payload = self._http.search_cql(cql, limit=page_limit, expand=applied_expand, start=current_start)
|
|
84
|
+
results = payload.get("results", [])
|
|
85
|
+
count = len(results)
|
|
86
|
+
if not results:
|
|
87
|
+
break
|
|
88
|
+
metrics.mark_processed(count)
|
|
89
|
+
metrics.scanned += count
|
|
90
|
+
for item in results:
|
|
91
|
+
fetched += 1
|
|
92
|
+
yield item
|
|
93
|
+
|
|
94
|
+
current_start = payload.get("start", current_start) + count
|
|
95
|
+
if max_results is not None and fetched >= max_results:
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
metrics.mark_finished()
|
|
99
|
+
self._last_metrics = metrics
|
|
100
|
+
|
|
101
|
+
def snapshot(
|
|
102
|
+
self,
|
|
103
|
+
cql: str,
|
|
104
|
+
*,
|
|
105
|
+
limit: int = 25,
|
|
106
|
+
max_results: int | None = None,
|
|
107
|
+
expand: Sequence[str] | None = None,
|
|
108
|
+
) -> dict[str, Any]:
|
|
109
|
+
"""Return an aggregated snapshot for reporting pipelines."""
|
|
110
|
+
|
|
111
|
+
results: list[dict[str, Any]] = []
|
|
112
|
+
for item in self.scan(cql, limit=limit, max_results=max_results, expand=expand):
|
|
113
|
+
results.append(item)
|
|
114
|
+
return {
|
|
115
|
+
"cql": cql,
|
|
116
|
+
"limit": limit,
|
|
117
|
+
"maxResults": max_results,
|
|
118
|
+
"count": len(results),
|
|
119
|
+
"results": results,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def last_metrics(self) -> BatchMetrics | None:
|
|
124
|
+
return self._last_metrics
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class WebhookClient:
|
|
128
|
+
"""Manage Confluence Server/DC webhooks via REST v1."""
|
|
129
|
+
|
|
130
|
+
_BASE_PATH = "/rest/webhooks/1.0/webhook"
|
|
131
|
+
|
|
132
|
+
def __init__(self, http_client: ConfluenceClient) -> None:
|
|
133
|
+
self._http = http_client
|
|
134
|
+
|
|
135
|
+
def create(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
name: str,
|
|
139
|
+
url: str,
|
|
140
|
+
events: Iterable[str],
|
|
141
|
+
active: bool = True,
|
|
142
|
+
filters: dict[str, Any] | None = None,
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
payload = {
|
|
145
|
+
"name": name,
|
|
146
|
+
"url": url,
|
|
147
|
+
"events": list(events),
|
|
148
|
+
"excludeBody": False,
|
|
149
|
+
"active": active,
|
|
150
|
+
}
|
|
151
|
+
if filters:
|
|
152
|
+
payload["filters"] = filters
|
|
153
|
+
response = self._http.request("POST", self._BASE_PATH, json=payload)
|
|
154
|
+
return response.json()
|
|
155
|
+
|
|
156
|
+
def list(self) -> builtins.list[dict[str, Any]]:
|
|
157
|
+
response = self._http.request("GET", self._BASE_PATH)
|
|
158
|
+
payload = response.json()
|
|
159
|
+
return payload if isinstance(payload, list) else payload.get("results", [])
|
|
160
|
+
|
|
161
|
+
def delete(self, webhook_id: str) -> None:
|
|
162
|
+
self._http.request("DELETE", f"{self._BASE_PATH}/{webhook_id}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
__all__ = ["BatchRunner", "BatchMetrics", "WebhookClient"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Run reporting utilities for the Confluence orchestrator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
TIMESTAMP_FMT = "%Y-%m-%dT%H-%M-%SZ"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class RunSummary:
|
|
15
|
+
command: str
|
|
16
|
+
args: list[str]
|
|
17
|
+
server: str
|
|
18
|
+
exit_code: int
|
|
19
|
+
duration_ms: int
|
|
20
|
+
started_at: datetime
|
|
21
|
+
finished_at: datetime
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def success(self) -> bool:
|
|
25
|
+
return self.exit_code == 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _ensure_directory(base: Path, timestamp: datetime) -> Path:
|
|
29
|
+
day_dir = base / str(timestamp.year) / f"{timestamp.month:02d}" / f"{timestamp.day:02d}"
|
|
30
|
+
day_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
return day_dir
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_reports(
|
|
35
|
+
summary: RunSummary,
|
|
36
|
+
*,
|
|
37
|
+
base_dir: Path,
|
|
38
|
+
include_markdown: bool = True,
|
|
39
|
+
) -> Path:
|
|
40
|
+
"""Persist JSON (and optional Markdown) reports and return JSON path."""
|
|
41
|
+
|
|
42
|
+
timestamp = summary.finished_at.astimezone(UTC)
|
|
43
|
+
directory = _ensure_directory(base_dir, timestamp)
|
|
44
|
+
slug = f"{timestamp.strftime(TIMESTAMP_FMT)}_{summary.command}"
|
|
45
|
+
|
|
46
|
+
json_path = directory / f"{slug}.json"
|
|
47
|
+
json_payload = {
|
|
48
|
+
"command": summary.command,
|
|
49
|
+
"args": summary.args,
|
|
50
|
+
"server": summary.server,
|
|
51
|
+
"exit_code": summary.exit_code,
|
|
52
|
+
"duration_ms": summary.duration_ms,
|
|
53
|
+
"started_at": summary.started_at.astimezone(UTC).isoformat().replace("+00:00", "Z"),
|
|
54
|
+
"finished_at": summary.finished_at.astimezone(UTC).isoformat().replace("+00:00", "Z"),
|
|
55
|
+
"success": summary.success,
|
|
56
|
+
}
|
|
57
|
+
json_path.write_text(json.dumps(json_payload, indent=2, sort_keys=True))
|
|
58
|
+
|
|
59
|
+
if include_markdown:
|
|
60
|
+
md_path = directory / f"{slug}.md"
|
|
61
|
+
status = "✅ Success" if summary.success else "❌ Failure"
|
|
62
|
+
md_lines = [
|
|
63
|
+
f"# Confluence Run Report — {summary.command}",
|
|
64
|
+
"",
|
|
65
|
+
f"- **Status**: {status} (exit code {summary.exit_code})",
|
|
66
|
+
f"- **Server**: {summary.server}",
|
|
67
|
+
f"- **Duration**: {summary.duration_ms} ms",
|
|
68
|
+
f"- **Started**: {json_payload['started_at']}",
|
|
69
|
+
f"- **Finished**: {json_payload['finished_at']}",
|
|
70
|
+
]
|
|
71
|
+
if summary.args:
|
|
72
|
+
md_lines.append(f"- **Arguments**: {' '.join(summary.args)}")
|
|
73
|
+
md_path.write_text("\n".join(md_lines) + "\n")
|
|
74
|
+
|
|
75
|
+
return json_path
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = ["RunSummary", "write_reports"]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Confluence page tree traversal and export utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
from .http import ConfluenceClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class TreeNode:
|
|
15
|
+
"""Represents a Confluence page in the tree."""
|
|
16
|
+
|
|
17
|
+
page_id: str
|
|
18
|
+
title: str
|
|
19
|
+
space_key: str
|
|
20
|
+
url: str
|
|
21
|
+
children: list[TreeNode] = field(default_factory=list)
|
|
22
|
+
attachments: list[dict[str, Any]] = field(default_factory=list)
|
|
23
|
+
body: str | None = None
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> dict[str, Any]:
|
|
26
|
+
"""Convert tree node to dictionary."""
|
|
27
|
+
return {
|
|
28
|
+
"id": self.page_id,
|
|
29
|
+
"title": self.title,
|
|
30
|
+
"space": self.space_key,
|
|
31
|
+
"url": self.url,
|
|
32
|
+
"body": self.body,
|
|
33
|
+
"attachments": self.attachments,
|
|
34
|
+
"children": [child.to_dict() for child in self.children],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TreeExporter:
|
|
39
|
+
"""Recursively fetch Confluence page tree."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, client: ConfluenceClient) -> None:
|
|
42
|
+
self._client = client
|
|
43
|
+
self._log = structlog.get_logger(__name__)
|
|
44
|
+
|
|
45
|
+
def build_tree(
|
|
46
|
+
self,
|
|
47
|
+
root_id: str,
|
|
48
|
+
*,
|
|
49
|
+
depth: int = -1,
|
|
50
|
+
include_content: bool = False,
|
|
51
|
+
include_attachments: bool = False,
|
|
52
|
+
) -> TreeNode:
|
|
53
|
+
"""
|
|
54
|
+
Recursively build a tree of pages.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
root_id: The root page ID to start from.
|
|
58
|
+
depth: How deep to recurse. -1 for infinite, 0 for just root.
|
|
59
|
+
include_content: Whether to fetch page body (storage format).
|
|
60
|
+
include_attachments: Whether to fetch attachment metadata.
|
|
61
|
+
"""
|
|
62
|
+
self._log.info("fetching_page", page_id=root_id)
|
|
63
|
+
|
|
64
|
+
# Prepare expand fields
|
|
65
|
+
expand = ["space"]
|
|
66
|
+
if include_content:
|
|
67
|
+
expand.append("body.storage")
|
|
68
|
+
|
|
69
|
+
# Fetch page details
|
|
70
|
+
page = self._client.get_page(root_id, expand=expand)
|
|
71
|
+
title = page.get("title", "Unknown")
|
|
72
|
+
space_key = page.get("space", {}).get("key", "Unknown")
|
|
73
|
+
links = page.get("_links", {})
|
|
74
|
+
base_url = links.get("base", "")
|
|
75
|
+
web_ui = links.get("webui", "")
|
|
76
|
+
full_url = f"{base_url}{web_ui}" if base_url and web_ui else web_ui
|
|
77
|
+
|
|
78
|
+
body = None
|
|
79
|
+
if include_content:
|
|
80
|
+
body = page.get("body", {}).get("storage", {}).get("value")
|
|
81
|
+
|
|
82
|
+
attachments = []
|
|
83
|
+
if include_attachments:
|
|
84
|
+
try:
|
|
85
|
+
# This fetches metadata, not content
|
|
86
|
+
attachments = self._client.get_attachments(root_id)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
self._log.warning("failed_fetching_attachments", page_id=root_id, error=str(e))
|
|
89
|
+
|
|
90
|
+
node = TreeNode(
|
|
91
|
+
page_id=root_id,
|
|
92
|
+
title=title,
|
|
93
|
+
space_key=space_key,
|
|
94
|
+
url=full_url,
|
|
95
|
+
children=[],
|
|
96
|
+
attachments=attachments,
|
|
97
|
+
body=body,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Recurse if depth allows
|
|
101
|
+
if depth != 0:
|
|
102
|
+
try:
|
|
103
|
+
# Fetch immediate children summaries
|
|
104
|
+
children_summaries = self._client.get_child_pages(root_id)
|
|
105
|
+
|
|
106
|
+
next_depth = depth - 1 if depth > 0 else -1
|
|
107
|
+
|
|
108
|
+
for child_summary in children_summaries:
|
|
109
|
+
child_id = child_summary.get("id")
|
|
110
|
+
if child_id:
|
|
111
|
+
child_node = self.build_tree(
|
|
112
|
+
child_id,
|
|
113
|
+
depth=next_depth,
|
|
114
|
+
include_content=include_content,
|
|
115
|
+
include_attachments=include_attachments,
|
|
116
|
+
)
|
|
117
|
+
node.children.append(child_node)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
self._log.warning("failed_fetching_children", page_id=root_id, error=str(e))
|
|
120
|
+
|
|
121
|
+
return node
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sync PDF files from markdown sources.
|
|
4
|
+
|
|
5
|
+
This script:
|
|
6
|
+
1. Scans directory for markdown files
|
|
7
|
+
2. Checks if corresponding PDF exists
|
|
8
|
+
3. Compares modification times
|
|
9
|
+
4. Regenerates PDFs using vds-cli pdf md2pdf when markdown is newer
|
|
10
|
+
5. Reports summary of regenerated/skipped files
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(help="Sync PDF files from markdown sources")
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
# Path to vds-scripts for running vds-cli (relative to script location)
|
|
29
|
+
VDS_SCRIPTS = Path(__file__).resolve().parent.parent
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_markdown_files(directory: Path, pattern: str | None = None, recursive: bool = True) -> list[Path]:
|
|
33
|
+
"""Find all markdown files in directory, optionally matching pattern."""
|
|
34
|
+
if recursive:
|
|
35
|
+
md_files = sorted(directory.rglob("*.md"))
|
|
36
|
+
else:
|
|
37
|
+
md_files = sorted(directory.glob("*.md"))
|
|
38
|
+
if pattern:
|
|
39
|
+
md_files = [f for f in md_files if pattern.lower() in f.name.lower()]
|
|
40
|
+
return md_files
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_corresponding_pdf(md_file: Path) -> Path:
|
|
44
|
+
"""Get corresponding PDF path for a markdown file."""
|
|
45
|
+
return md_file.with_suffix(".pdf")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def needs_regeneration(md_file: Path, pdf_file: Path, force: bool = False) -> bool:
|
|
49
|
+
"""Check if PDF needs regeneration based on modification times."""
|
|
50
|
+
if force:
|
|
51
|
+
return True
|
|
52
|
+
if not pdf_file.exists():
|
|
53
|
+
return True
|
|
54
|
+
# Regenerate if markdown is newer than PDF
|
|
55
|
+
md_mtime = md_file.stat().st_mtime
|
|
56
|
+
pdf_mtime = pdf_file.stat().st_mtime
|
|
57
|
+
return md_mtime > pdf_mtime
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def regenerate_pdf(md_file: Path, pdf_file: Path, dry_run: bool = False) -> tuple[bool, str]:
|
|
61
|
+
"""Regenerate PDF from markdown using vds-cli pdf md2pdf."""
|
|
62
|
+
if dry_run:
|
|
63
|
+
return True, "Would regenerate (dry-run)"
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Ensure output directory exists
|
|
67
|
+
pdf_file.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
# Call vds-cli pdf md2pdf
|
|
70
|
+
cmd = [
|
|
71
|
+
"uv",
|
|
72
|
+
"run",
|
|
73
|
+
"--project",
|
|
74
|
+
"vds_cli",
|
|
75
|
+
"vds-cli",
|
|
76
|
+
"pdf",
|
|
77
|
+
"--",
|
|
78
|
+
"md2pdf",
|
|
79
|
+
str(md_file),
|
|
80
|
+
"--output",
|
|
81
|
+
str(pdf_file),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
result = subprocess.run(
|
|
85
|
+
cmd,
|
|
86
|
+
cwd=str(VDS_SCRIPTS),
|
|
87
|
+
capture_output=True,
|
|
88
|
+
text=True,
|
|
89
|
+
timeout=300, # 5 minute timeout
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if result.returncode == 0:
|
|
93
|
+
return True, "Regenerated successfully"
|
|
94
|
+
else:
|
|
95
|
+
error_msg = result.stderr.strip() or result.stdout.strip()
|
|
96
|
+
return False, f"Error: {error_msg}"
|
|
97
|
+
|
|
98
|
+
except subprocess.TimeoutExpired:
|
|
99
|
+
return False, "Timeout after 5 minutes"
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return False, f"Exception: {str(e)}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command()
|
|
105
|
+
def sync(
|
|
106
|
+
directory: Path = typer.Argument(
|
|
107
|
+
Path("."), help="Directory to scan for markdown files"
|
|
108
|
+
),
|
|
109
|
+
pattern: str | None = typer.Option(None, "--pattern", "-p", help="Filter markdown files by pattern"),
|
|
110
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview changes without regenerating PDFs"),
|
|
111
|
+
force: bool = typer.Option(False, "--force", help="Regenerate all PDFs regardless of timestamps"),
|
|
112
|
+
recursive: bool = typer.Option(True, "--recursive/--no-recursive", help="Scan subdirectories recursively"),
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Sync PDFs from markdown sources."""
|
|
115
|
+
|
|
116
|
+
directory = directory.resolve()
|
|
117
|
+
if not directory.exists() or not directory.is_dir():
|
|
118
|
+
console.print(f"[red]Error: Directory not found: {directory}[/red]")
|
|
119
|
+
raise typer.Exit(code=1) from None
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
# Find markdown files
|
|
123
|
+
console.print(f"[bold]Scanning markdown files in:[/bold] {directory}")
|
|
124
|
+
md_files = find_markdown_files(directory, pattern, recursive)
|
|
125
|
+
|
|
126
|
+
if not md_files:
|
|
127
|
+
console.print("[yellow]No markdown files found[/yellow]")
|
|
128
|
+
raise typer.Exit(code=0) from None
|
|
129
|
+
|
|
130
|
+
console.print(f"Found [green]{len(md_files)}[/green] markdown file(s)")
|
|
131
|
+
|
|
132
|
+
# Check each markdown file
|
|
133
|
+
to_regenerate: list[tuple[Path, Path]] = []
|
|
134
|
+
skipped: list[tuple[Path, Path]] = []
|
|
135
|
+
errors: list[tuple[Path, str]] = []
|
|
136
|
+
|
|
137
|
+
console.print("\n[bold]Checking PDF status...[/bold]")
|
|
138
|
+
for md_file in md_files:
|
|
139
|
+
pdf_file = get_corresponding_pdf(md_file)
|
|
140
|
+
if needs_regeneration(md_file, pdf_file, force):
|
|
141
|
+
to_regenerate.append((md_file, pdf_file))
|
|
142
|
+
else:
|
|
143
|
+
skipped.append((md_file, pdf_file))
|
|
144
|
+
|
|
145
|
+
# Display sync plan
|
|
146
|
+
table = Table(title="Sync Plan", show_header=True, header_style="bold magenta")
|
|
147
|
+
table.add_column("Markdown File", style="cyan")
|
|
148
|
+
table.add_column("PDF File", style="green")
|
|
149
|
+
table.add_column("Action", style="yellow")
|
|
150
|
+
|
|
151
|
+
for md_file, pdf_file in to_regenerate:
|
|
152
|
+
status = "REGENERATE" if pdf_file.exists() else "CREATE"
|
|
153
|
+
table.add_row(
|
|
154
|
+
str(md_file.relative_to(directory)),
|
|
155
|
+
str(pdf_file.relative_to(directory)),
|
|
156
|
+
f"[yellow]{status}[/yellow]",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
for md_file, pdf_file in skipped:
|
|
160
|
+
table.add_row(
|
|
161
|
+
str(md_file.relative_to(directory)),
|
|
162
|
+
str(pdf_file.relative_to(directory)),
|
|
163
|
+
"[green]SKIP (in sync)[/green]",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if to_regenerate:
|
|
167
|
+
console.print(table)
|
|
168
|
+
console.print(f"\n[bold]Summary:[/bold] {len(to_regenerate)} to regenerate, {len(skipped)} skipped")
|
|
169
|
+
|
|
170
|
+
if dry_run:
|
|
171
|
+
console.print("\n[yellow]DRY RUN: No PDFs regenerated[/yellow]")
|
|
172
|
+
raise typer.Exit(code=0) from None
|
|
173
|
+
|
|
174
|
+
# Regenerate PDFs
|
|
175
|
+
console.print("\n[bold]Regenerating PDFs...[/bold]")
|
|
176
|
+
|
|
177
|
+
with Progress(
|
|
178
|
+
SpinnerColumn(),
|
|
179
|
+
TextColumn("[progress.description]{task.description}"),
|
|
180
|
+
console=console,
|
|
181
|
+
) as progress:
|
|
182
|
+
for md_file, pdf_file in to_regenerate:
|
|
183
|
+
task = progress.add_task(f"Regenerating {md_file.name}...", total=None)
|
|
184
|
+
|
|
185
|
+
success, message = regenerate_pdf(md_file, pdf_file, dry_run=False)
|
|
186
|
+
progress.update(task, completed=True)
|
|
187
|
+
|
|
188
|
+
if success:
|
|
189
|
+
console.print(f" [green]✓[/green] {md_file.name} → {pdf_file.name}")
|
|
190
|
+
else:
|
|
191
|
+
console.print(f" [red]✗[/red] {md_file.name}: {message}")
|
|
192
|
+
errors.append((md_file, message))
|
|
193
|
+
|
|
194
|
+
if errors:
|
|
195
|
+
console.print(f"\n[yellow]Completed with {len(errors)} error(s)[/yellow]")
|
|
196
|
+
for md_file, error_msg in errors:
|
|
197
|
+
console.print(f" • {md_file.name}: {error_msg}")
|
|
198
|
+
raise typer.Exit(code=2) from None
|
|
199
|
+
else:
|
|
200
|
+
console.print(f"\n[bold green]✓ All PDFs regenerated successfully![/bold green]")
|
|
201
|
+
else:
|
|
202
|
+
console.print("\n[green]All PDFs are in sync with markdown sources[/green]")
|
|
203
|
+
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
console.print(f"[red]Error: {exc}[/red]")
|
|
206
|
+
raise typer.Exit(code=1) from None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
app()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|