@ngocsangairvds/vsaf 3.1.27 → 3.2.2
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 +65 -39
- 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
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sync local PDF files with Confluence page attachments.
|
|
4
|
+
|
|
5
|
+
This script:
|
|
6
|
+
1. Lists local PDF files from a directory
|
|
7
|
+
2. Lists current attachments on a Confluence page
|
|
8
|
+
3. Matches PDF attachments by filename
|
|
9
|
+
4. Uploads/replaces PDF files on Confluence page
|
|
10
|
+
5. Optionally compares modification times with markdown sources
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from confluence_orchestrator.config import load_settings
|
|
20
|
+
from confluence_orchestrator.content import ContentClient
|
|
21
|
+
from confluence_orchestrator.http import ConfluenceClient
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(help="Sync local PDF files with Confluence page attachments")
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_http_client(settings, server: str) -> ConfluenceClient:
|
|
31
|
+
"""Build HTTP client for Confluence."""
|
|
32
|
+
return ConfluenceClient(settings, server=server)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_content_client(settings, server: str) -> ContentClient:
|
|
36
|
+
"""Build content client for Confluence."""
|
|
37
|
+
http_client = _build_http_client(settings, server)
|
|
38
|
+
return ContentClient(http_client)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def find_pdf_files(directory: Path, pattern: str | None = None, recursive: bool = True) -> list[Path]:
|
|
42
|
+
"""Find all PDF files in directory, optionally matching pattern."""
|
|
43
|
+
if recursive:
|
|
44
|
+
pdf_files = sorted(directory.rglob("*.pdf"))
|
|
45
|
+
else:
|
|
46
|
+
pdf_files = sorted(directory.glob("*.pdf"))
|
|
47
|
+
if pattern:
|
|
48
|
+
pdf_files = [f for f in pdf_files if pattern.lower() in f.name.lower()]
|
|
49
|
+
return pdf_files
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def match_attachments(
|
|
53
|
+
local_pdfs: list[Path], confluence_attachments: list[dict[str, Any]]
|
|
54
|
+
) -> dict[str, dict[str, Any]]:
|
|
55
|
+
"""Match local PDF files with Confluence attachments by filename."""
|
|
56
|
+
matches: dict[str, dict[str, Any]] = {}
|
|
57
|
+
|
|
58
|
+
# Create lookup by filename (case-insensitive)
|
|
59
|
+
attachment_lookup: dict[str, dict[str, Any]] = {}
|
|
60
|
+
for att in confluence_attachments:
|
|
61
|
+
filename = att.get("title", "").lower()
|
|
62
|
+
attachment_lookup[filename] = att
|
|
63
|
+
|
|
64
|
+
# Match local files to attachments
|
|
65
|
+
for local_file in local_pdfs:
|
|
66
|
+
filename_lower = local_file.name.lower()
|
|
67
|
+
if filename_lower in attachment_lookup:
|
|
68
|
+
matches[local_file.name] = {
|
|
69
|
+
"local_file": local_file,
|
|
70
|
+
"attachment": attachment_lookup[filename_lower],
|
|
71
|
+
"action": "replace",
|
|
72
|
+
}
|
|
73
|
+
else:
|
|
74
|
+
matches[local_file.name] = {
|
|
75
|
+
"local_file": local_file,
|
|
76
|
+
"attachment": None,
|
|
77
|
+
"action": "upload",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return matches
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command("sync")
|
|
84
|
+
def sync_pdfs(
|
|
85
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
86
|
+
pdf_directory: Path = typer.Option(..., "--dir", "-d", help="Directory containing PDF files"),
|
|
87
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
88
|
+
pattern: str | None = typer.Option(None, "--pattern", "-p", help="Filter PDF files by pattern"),
|
|
89
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without making changes"),
|
|
90
|
+
comment: str | None = typer.Option(None, "--comment", "-c", help="Comment for attachment updates"),
|
|
91
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Auto-confirm without prompting"),
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Sync local PDF files with Confluence page attachments."""
|
|
94
|
+
|
|
95
|
+
if not pdf_directory.exists() or not pdf_directory.is_dir():
|
|
96
|
+
console.print(f"[red]Error: Directory not found: {pdf_directory}[/red]")
|
|
97
|
+
raise typer.Exit(code=1) from None
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
settings = load_settings(strict=True)
|
|
101
|
+
server_key = server or settings.default_server
|
|
102
|
+
client = _build_content_client(settings, server_key)
|
|
103
|
+
|
|
104
|
+
# Get page info
|
|
105
|
+
with console.status("[bold green]Fetching Confluence page info..."):
|
|
106
|
+
page_info = client.get_page(page_id)
|
|
107
|
+
page_title = page_info.get("title", "Unknown")
|
|
108
|
+
|
|
109
|
+
console.print(f"\n[bold]Page:[/bold] {page_title} (ID: {page_id})")
|
|
110
|
+
console.print(f"[bold]Server:[/bold] {server_key}\n")
|
|
111
|
+
|
|
112
|
+
# List local PDF files
|
|
113
|
+
console.print(f"[bold]Scanning PDF files in:[/bold] {pdf_directory}")
|
|
114
|
+
local_pdfs = find_pdf_files(pdf_directory, pattern)
|
|
115
|
+
|
|
116
|
+
if not local_pdfs:
|
|
117
|
+
console.print("[yellow]No PDF files found in directory[/yellow]")
|
|
118
|
+
raise typer.Exit(code=0) from None
|
|
119
|
+
|
|
120
|
+
console.print(f"Found [green]{len(local_pdfs)}[/green] PDF file(s):")
|
|
121
|
+
for pdf in local_pdfs:
|
|
122
|
+
size = pdf.stat().st_size
|
|
123
|
+
console.print(f" • {pdf.name} ({size:,} bytes)")
|
|
124
|
+
|
|
125
|
+
# List Confluence attachments
|
|
126
|
+
console.print("\n[bold]Fetching Confluence attachments...[/bold]")
|
|
127
|
+
attachments_result = client.list_attachments(page_id)
|
|
128
|
+
# Handle both list and dict responses
|
|
129
|
+
if isinstance(attachments_result, list):
|
|
130
|
+
attachments = attachments_result
|
|
131
|
+
elif isinstance(attachments_result, dict):
|
|
132
|
+
attachments = attachments_result.get("results", [])
|
|
133
|
+
else:
|
|
134
|
+
attachments = []
|
|
135
|
+
|
|
136
|
+
# Filter PDF attachments
|
|
137
|
+
pdf_attachments = [
|
|
138
|
+
att for att in attachments
|
|
139
|
+
if att.get("title", "").lower().endswith(".pdf")
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
console.print(f"Found [green]{len(pdf_attachments)}[/green] PDF attachment(s) on page:")
|
|
143
|
+
for att in pdf_attachments:
|
|
144
|
+
name = att.get("title", "Unknown")
|
|
145
|
+
size = att.get("size", 0)
|
|
146
|
+
att_id = att.get("id", "Unknown")
|
|
147
|
+
console.print(f" • {name} (ID: {att_id}, {size:,} bytes)")
|
|
148
|
+
|
|
149
|
+
# Match local files with attachments
|
|
150
|
+
console.print("\n[bold]Matching files...[/bold]")
|
|
151
|
+
matches = match_attachments(local_pdfs, pdf_attachments)
|
|
152
|
+
|
|
153
|
+
# Display sync plan
|
|
154
|
+
table = Table(title="Sync Plan", show_header=True, header_style="bold magenta")
|
|
155
|
+
table.add_column("Local File", style="cyan")
|
|
156
|
+
table.add_column("Action", style="yellow")
|
|
157
|
+
table.add_column("Confluence Attachment", style="green")
|
|
158
|
+
table.add_column("Size", justify="right")
|
|
159
|
+
|
|
160
|
+
replace_count = 0
|
|
161
|
+
upload_count = 0
|
|
162
|
+
|
|
163
|
+
for filename, match_info in matches.items():
|
|
164
|
+
local_file = match_info["local_file"]
|
|
165
|
+
attachment = match_info["attachment"]
|
|
166
|
+
action = match_info["action"]
|
|
167
|
+
size = local_file.stat().st_size
|
|
168
|
+
|
|
169
|
+
if action == "replace":
|
|
170
|
+
replace_count += 1
|
|
171
|
+
att_name = attachment.get("title", "Unknown") if attachment else "N/A"
|
|
172
|
+
table.add_row(filename, "[yellow]REPLACE[/yellow]", att_name, f"{size:,} bytes")
|
|
173
|
+
else:
|
|
174
|
+
upload_count += 1
|
|
175
|
+
table.add_row(filename, "[green]UPLOAD NEW[/green]", "—", f"{size:,} bytes")
|
|
176
|
+
|
|
177
|
+
console.print(table)
|
|
178
|
+
console.print(f"\n[bold]Summary:[/bold] {replace_count} to replace, {upload_count} to upload")
|
|
179
|
+
|
|
180
|
+
if dry_run:
|
|
181
|
+
console.print("\n[yellow]DRY RUN: No changes made[/yellow]")
|
|
182
|
+
raise typer.Exit(code=0) from None
|
|
183
|
+
|
|
184
|
+
# Confirm before proceeding
|
|
185
|
+
if not yes:
|
|
186
|
+
if not typer.confirm("\n[bold]Proceed with sync?[/bold]"):
|
|
187
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
188
|
+
raise typer.Exit(code=0) from None
|
|
189
|
+
|
|
190
|
+
# Perform sync
|
|
191
|
+
console.print("\n[bold]Syncing attachments...[/bold]")
|
|
192
|
+
|
|
193
|
+
with Progress(
|
|
194
|
+
SpinnerColumn(),
|
|
195
|
+
TextColumn("[progress.description]{task.description}"),
|
|
196
|
+
console=console,
|
|
197
|
+
) as progress:
|
|
198
|
+
# Replace existing attachments
|
|
199
|
+
for filename, match_info in matches.items():
|
|
200
|
+
if match_info["action"] == "replace":
|
|
201
|
+
local_file = match_info["local_file"]
|
|
202
|
+
attachment = match_info["attachment"]
|
|
203
|
+
att_id = attachment.get("id")
|
|
204
|
+
|
|
205
|
+
task = progress.add_task(f"Replacing {filename}...", total=None)
|
|
206
|
+
|
|
207
|
+
# Update existing attachment
|
|
208
|
+
try:
|
|
209
|
+
client.update_attachment(
|
|
210
|
+
page_id,
|
|
211
|
+
att_id,
|
|
212
|
+
str(local_file),
|
|
213
|
+
filename=local_file.name,
|
|
214
|
+
content_type="application/pdf",
|
|
215
|
+
comment=comment or f"Updated from local file: {local_file.name}",
|
|
216
|
+
)
|
|
217
|
+
progress.update(task, completed=True)
|
|
218
|
+
console.print(f" [green]✓[/green] Replaced {filename}")
|
|
219
|
+
except Exception as e:
|
|
220
|
+
progress.update(task, completed=True)
|
|
221
|
+
console.print(f" [red]✗[/red] Failed to replace {filename}: {e}")
|
|
222
|
+
|
|
223
|
+
# Upload new attachments
|
|
224
|
+
for filename, match_info in matches.items():
|
|
225
|
+
if match_info["action"] == "upload":
|
|
226
|
+
local_file = match_info["local_file"]
|
|
227
|
+
|
|
228
|
+
task = progress.add_task(f"Uploading {filename}...", total=None)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
client.upload_attachment(
|
|
232
|
+
page_id,
|
|
233
|
+
str(local_file),
|
|
234
|
+
filename=local_file.name,
|
|
235
|
+
content_type="application/pdf",
|
|
236
|
+
comment=comment or f"Uploaded: {local_file.name}",
|
|
237
|
+
)
|
|
238
|
+
progress.update(task, completed=True)
|
|
239
|
+
console.print(f" [green]✓[/green] Uploaded {filename}")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
progress.update(task, completed=True)
|
|
242
|
+
console.print(f" [red]✗[/red] Failed to upload {filename}: {e}")
|
|
243
|
+
|
|
244
|
+
console.print("\n[bold green]Sync completed![/bold green]")
|
|
245
|
+
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
console.print(f"[red]Error: {exc}[/red]")
|
|
248
|
+
raise typer.Exit(code=1) from None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@app.command("list")
|
|
252
|
+
def list_attachments(
|
|
253
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
254
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
255
|
+
pdf_only: bool = typer.Option(False, "--pdf-only", help="Show only PDF attachments"),
|
|
256
|
+
) -> None:
|
|
257
|
+
"""List attachments on a Confluence page."""
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
settings = load_settings(strict=True)
|
|
261
|
+
server_key = server or settings.default_server
|
|
262
|
+
client = _build_content_client(settings, server_key)
|
|
263
|
+
|
|
264
|
+
# Get page info
|
|
265
|
+
page_info = client.get_page(page_id)
|
|
266
|
+
page_title = page_info.get("title", "Unknown")
|
|
267
|
+
|
|
268
|
+
console.print(f"\n[bold]Page:[/bold] {page_title} (ID: {page_id})")
|
|
269
|
+
console.print(f"[bold]Server:[/bold] {server_key}\n")
|
|
270
|
+
|
|
271
|
+
# List attachments
|
|
272
|
+
attachments_result = client.list_attachments(page_id)
|
|
273
|
+
attachments = attachments_result if isinstance(attachments_result, list) else attachments_result.get("results", [])
|
|
274
|
+
|
|
275
|
+
if pdf_only:
|
|
276
|
+
attachments = [att for att in attachments if att.get("title", "").lower().endswith(".pdf")]
|
|
277
|
+
|
|
278
|
+
if not attachments:
|
|
279
|
+
console.print("[yellow]No attachments found[/yellow]")
|
|
280
|
+
raise typer.Exit(code=0) from None
|
|
281
|
+
|
|
282
|
+
table = Table(title="Attachments", show_header=True, header_style="bold magenta")
|
|
283
|
+
table.add_column("Filename", style="cyan")
|
|
284
|
+
table.add_column("ID", style="green")
|
|
285
|
+
table.add_column("Size", justify="right")
|
|
286
|
+
table.add_column("Type", style="yellow")
|
|
287
|
+
|
|
288
|
+
for att in attachments:
|
|
289
|
+
name = att.get("title", "Unknown")
|
|
290
|
+
att_id = att.get("id", "Unknown")
|
|
291
|
+
size = att.get("size", 0)
|
|
292
|
+
mime_type = att.get("metadata", {}).get("mediaType", "unknown")
|
|
293
|
+
|
|
294
|
+
table.add_row(name, att_id, f"{size:,} bytes", mime_type)
|
|
295
|
+
|
|
296
|
+
console.print(table)
|
|
297
|
+
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
console.print(f"[red]Error: {exc}[/red]")
|
|
300
|
+
raise typer.Exit(code=1) from None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
if __name__ == "__main__":
|
|
304
|
+
app()
|
|
305
|
+
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sync local PNG files with Confluence page attachments.
|
|
4
|
+
|
|
5
|
+
This script:
|
|
6
|
+
1. Lists local PNG files from a directory
|
|
7
|
+
2. Lists current attachments on a Confluence page
|
|
8
|
+
3. Matches PNG attachments by filename
|
|
9
|
+
4. Uploads/replaces PNG files on Confluence page
|
|
10
|
+
5. Optionally rearranges attachments to match local order
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from confluence_orchestrator.config import load_settings
|
|
20
|
+
from confluence_orchestrator.content import ContentClient
|
|
21
|
+
from confluence_orchestrator.http import ConfluenceClient
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(help="Sync local PNG files with Confluence page attachments")
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_http_client(settings, server: str) -> ConfluenceClient:
|
|
31
|
+
"""Build HTTP client for Confluence."""
|
|
32
|
+
return ConfluenceClient(settings, server=server)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_content_client(settings, server: str) -> ContentClient:
|
|
36
|
+
"""Build content client for Confluence."""
|
|
37
|
+
http_client = _build_http_client(settings, server)
|
|
38
|
+
return ContentClient(http_client)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def find_png_files(directory: Path, pattern: str | None = None, recursive: bool = True) -> list[Path]:
|
|
42
|
+
"""Find all PNG files in directory, optionally matching pattern."""
|
|
43
|
+
if recursive:
|
|
44
|
+
png_files = sorted(directory.rglob("*.png"))
|
|
45
|
+
else:
|
|
46
|
+
png_files = sorted(directory.glob("*.png"))
|
|
47
|
+
if pattern:
|
|
48
|
+
png_files = [f for f in png_files if pattern.lower() in f.name.lower()]
|
|
49
|
+
return png_files
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def match_attachments(
|
|
53
|
+
local_pngs: list[Path], confluence_attachments: list[dict[str, Any]]
|
|
54
|
+
) -> dict[str, dict[str, Any]]:
|
|
55
|
+
"""Match local PNG files with Confluence attachments by filename."""
|
|
56
|
+
matches: dict[str, dict[str, Any]] = {}
|
|
57
|
+
|
|
58
|
+
# Create lookup by filename (case-insensitive)
|
|
59
|
+
attachment_lookup: dict[str, dict[str, Any]] = {}
|
|
60
|
+
for att in confluence_attachments:
|
|
61
|
+
filename = att.get("title", "").lower()
|
|
62
|
+
attachment_lookup[filename] = att
|
|
63
|
+
|
|
64
|
+
# Match local files to attachments
|
|
65
|
+
for local_file in local_pngs:
|
|
66
|
+
filename_lower = local_file.name.lower()
|
|
67
|
+
if filename_lower in attachment_lookup:
|
|
68
|
+
matches[local_file.name] = {
|
|
69
|
+
"local_file": local_file,
|
|
70
|
+
"attachment": attachment_lookup[filename_lower],
|
|
71
|
+
"action": "replace",
|
|
72
|
+
}
|
|
73
|
+
else:
|
|
74
|
+
matches[local_file.name] = {
|
|
75
|
+
"local_file": local_file,
|
|
76
|
+
"attachment": None,
|
|
77
|
+
"action": "upload",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return matches
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command("sync")
|
|
84
|
+
def sync_pngs(
|
|
85
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
86
|
+
png_directory: Path = typer.Option(..., "--dir", "-d", help="Directory containing PNG files"),
|
|
87
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
88
|
+
pattern: str | None = typer.Option(None, "--pattern", "-p", help="Filter PNG files by pattern"),
|
|
89
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without making changes"),
|
|
90
|
+
comment: str | None = typer.Option(None, "--comment", "-c", help="Comment for attachment updates"),
|
|
91
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Auto-confirm without prompting"),
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Sync local PNG files with Confluence page attachments."""
|
|
94
|
+
|
|
95
|
+
if not png_directory.exists() or not png_directory.is_dir():
|
|
96
|
+
console.print(f"[red]Error: Directory not found: {png_directory}[/red]")
|
|
97
|
+
raise typer.Exit(code=1) from None
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
settings = load_settings(strict=True)
|
|
101
|
+
server_key = server or settings.default_server
|
|
102
|
+
client = _build_content_client(settings, server_key)
|
|
103
|
+
|
|
104
|
+
# Get page info
|
|
105
|
+
with console.status("[bold green]Fetching Confluence page info..."):
|
|
106
|
+
page_info = client.get_page(page_id)
|
|
107
|
+
page_title = page_info.get("title", "Unknown")
|
|
108
|
+
|
|
109
|
+
console.print(f"\n[bold]Page:[/bold] {page_title} (ID: {page_id})")
|
|
110
|
+
console.print(f"[bold]Server:[/bold] {server_key}\n")
|
|
111
|
+
|
|
112
|
+
# List local PNG files
|
|
113
|
+
console.print(f"[bold]Scanning PNG files in:[/bold] {png_directory}")
|
|
114
|
+
local_pngs = find_png_files(png_directory, pattern)
|
|
115
|
+
|
|
116
|
+
if not local_pngs:
|
|
117
|
+
console.print("[yellow]No PNG files found in directory[/yellow]")
|
|
118
|
+
raise typer.Exit(code=0) from None
|
|
119
|
+
|
|
120
|
+
console.print(f"Found [green]{len(local_pngs)}[/green] PNG file(s):")
|
|
121
|
+
for png in local_pngs:
|
|
122
|
+
size = png.stat().st_size
|
|
123
|
+
console.print(f" • {png.name} ({size:,} bytes)")
|
|
124
|
+
|
|
125
|
+
# List Confluence attachments
|
|
126
|
+
console.print("\n[bold]Fetching Confluence attachments...[/bold]")
|
|
127
|
+
attachments_result = client.list_attachments(page_id)
|
|
128
|
+
# Handle both list and dict responses
|
|
129
|
+
if isinstance(attachments_result, list):
|
|
130
|
+
attachments = attachments_result
|
|
131
|
+
elif isinstance(attachments_result, dict):
|
|
132
|
+
attachments = attachments_result.get("results", [])
|
|
133
|
+
else:
|
|
134
|
+
attachments = []
|
|
135
|
+
|
|
136
|
+
# Filter PNG attachments
|
|
137
|
+
png_attachments = [
|
|
138
|
+
att for att in attachments
|
|
139
|
+
if att.get("title", "").lower().endswith(".png")
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
console.print(f"Found [green]{len(png_attachments)}[/green] PNG attachment(s) on page:")
|
|
143
|
+
for att in png_attachments:
|
|
144
|
+
name = att.get("title", "Unknown")
|
|
145
|
+
size = att.get("size", 0)
|
|
146
|
+
att_id = att.get("id", "Unknown")
|
|
147
|
+
console.print(f" • {name} (ID: {att_id}, {size:,} bytes)")
|
|
148
|
+
|
|
149
|
+
# Match local files with attachments
|
|
150
|
+
console.print("\n[bold]Matching files...[/bold]")
|
|
151
|
+
matches = match_attachments(local_pngs, png_attachments)
|
|
152
|
+
|
|
153
|
+
# Display sync plan
|
|
154
|
+
table = Table(title="Sync Plan", show_header=True, header_style="bold magenta")
|
|
155
|
+
table.add_column("Local File", style="cyan")
|
|
156
|
+
table.add_column("Action", style="yellow")
|
|
157
|
+
table.add_column("Confluence Attachment", style="green")
|
|
158
|
+
table.add_column("Size", justify="right")
|
|
159
|
+
|
|
160
|
+
replace_count = 0
|
|
161
|
+
upload_count = 0
|
|
162
|
+
|
|
163
|
+
for filename, match_info in matches.items():
|
|
164
|
+
local_file = match_info["local_file"]
|
|
165
|
+
attachment = match_info["attachment"]
|
|
166
|
+
action = match_info["action"]
|
|
167
|
+
size = local_file.stat().st_size
|
|
168
|
+
|
|
169
|
+
if action == "replace":
|
|
170
|
+
replace_count += 1
|
|
171
|
+
att_name = attachment.get("title", "Unknown") if attachment else "N/A"
|
|
172
|
+
table.add_row(filename, "[yellow]REPLACE[/yellow]", att_name, f"{size:,} bytes")
|
|
173
|
+
else:
|
|
174
|
+
upload_count += 1
|
|
175
|
+
table.add_row(filename, "[green]UPLOAD NEW[/green]", "—", f"{size:,} bytes")
|
|
176
|
+
|
|
177
|
+
console.print(table)
|
|
178
|
+
console.print(f"\n[bold]Summary:[/bold] {replace_count} to replace, {upload_count} to upload")
|
|
179
|
+
|
|
180
|
+
if dry_run:
|
|
181
|
+
console.print("\n[yellow]DRY RUN: No changes made[/yellow]")
|
|
182
|
+
raise typer.Exit(code=0) from None
|
|
183
|
+
|
|
184
|
+
# Confirm before proceeding
|
|
185
|
+
if not yes:
|
|
186
|
+
if not typer.confirm("\n[bold]Proceed with sync?[/bold]"):
|
|
187
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
188
|
+
raise typer.Exit(code=0) from None
|
|
189
|
+
|
|
190
|
+
# Perform sync
|
|
191
|
+
console.print("\n[bold]Syncing attachments...[/bold]")
|
|
192
|
+
|
|
193
|
+
with Progress(
|
|
194
|
+
SpinnerColumn(),
|
|
195
|
+
TextColumn("[progress.description]{task.description}"),
|
|
196
|
+
console=console,
|
|
197
|
+
) as progress:
|
|
198
|
+
# Replace existing attachments
|
|
199
|
+
for filename, match_info in matches.items():
|
|
200
|
+
if match_info["action"] == "replace":
|
|
201
|
+
local_file = match_info["local_file"]
|
|
202
|
+
attachment = match_info["attachment"]
|
|
203
|
+
att_id = attachment.get("id")
|
|
204
|
+
|
|
205
|
+
task = progress.add_task(f"Replacing {filename}...", total=None)
|
|
206
|
+
|
|
207
|
+
# Update existing attachment
|
|
208
|
+
try:
|
|
209
|
+
client.update_attachment(
|
|
210
|
+
page_id,
|
|
211
|
+
att_id,
|
|
212
|
+
str(local_file),
|
|
213
|
+
filename=local_file.name,
|
|
214
|
+
content_type="image/png",
|
|
215
|
+
comment=comment or f"Updated from local file: {local_file.name}",
|
|
216
|
+
)
|
|
217
|
+
progress.update(task, completed=True)
|
|
218
|
+
console.print(f" [green]✓[/green] Replaced {filename}")
|
|
219
|
+
except Exception as e:
|
|
220
|
+
progress.update(task, completed=True)
|
|
221
|
+
console.print(f" [red]✗[/red] Failed to replace {filename}: {e}")
|
|
222
|
+
|
|
223
|
+
# Upload new attachments
|
|
224
|
+
for filename, match_info in matches.items():
|
|
225
|
+
if match_info["action"] == "upload":
|
|
226
|
+
local_file = match_info["local_file"]
|
|
227
|
+
|
|
228
|
+
task = progress.add_task(f"Uploading {filename}...", total=None)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
client.upload_attachment(
|
|
232
|
+
page_id,
|
|
233
|
+
str(local_file),
|
|
234
|
+
filename=local_file.name,
|
|
235
|
+
content_type="image/png",
|
|
236
|
+
comment=comment or f"Uploaded: {local_file.name}",
|
|
237
|
+
)
|
|
238
|
+
progress.update(task, completed=True)
|
|
239
|
+
console.print(f" [green]✓[/green] Uploaded {filename}")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
progress.update(task, completed=True)
|
|
242
|
+
console.print(f" [red]✗[/red] Failed to upload {filename}: {e}")
|
|
243
|
+
|
|
244
|
+
console.print("\n[bold green]Sync completed![/bold green]")
|
|
245
|
+
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
console.print(f"[red]Error: {exc}[/red]")
|
|
248
|
+
raise typer.Exit(code=1) from None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@app.command("list")
|
|
252
|
+
def list_attachments(
|
|
253
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
254
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
255
|
+
png_only: bool = typer.Option(False, "--png-only", help="Show only PNG attachments"),
|
|
256
|
+
) -> None:
|
|
257
|
+
"""List attachments on a Confluence page."""
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
settings = load_settings(strict=True)
|
|
261
|
+
server_key = server or settings.default_server
|
|
262
|
+
client = _build_content_client(settings, server_key)
|
|
263
|
+
|
|
264
|
+
# Get page info
|
|
265
|
+
page_info = client.get_page(page_id)
|
|
266
|
+
page_title = page_info.get("title", "Unknown")
|
|
267
|
+
|
|
268
|
+
console.print(f"\n[bold]Page:[/bold] {page_title} (ID: {page_id})")
|
|
269
|
+
console.print(f"[bold]Server:[/bold] {server_key}\n")
|
|
270
|
+
|
|
271
|
+
# List attachments
|
|
272
|
+
attachments_result = client.list_attachments(page_id)
|
|
273
|
+
attachments = attachments_result if isinstance(attachments_result, list) else attachments_result.get("results", [])
|
|
274
|
+
|
|
275
|
+
if png_only:
|
|
276
|
+
attachments = [att for att in attachments if att.get("title", "").lower().endswith(".png")]
|
|
277
|
+
|
|
278
|
+
if not attachments:
|
|
279
|
+
console.print("[yellow]No attachments found[/yellow]")
|
|
280
|
+
raise typer.Exit(code=0) from None
|
|
281
|
+
|
|
282
|
+
table = Table(title="Attachments", show_header=True, header_style="bold magenta")
|
|
283
|
+
table.add_column("Filename", style="cyan")
|
|
284
|
+
table.add_column("ID", style="green")
|
|
285
|
+
table.add_column("Size", justify="right")
|
|
286
|
+
table.add_column("Type", style="yellow")
|
|
287
|
+
|
|
288
|
+
for att in attachments:
|
|
289
|
+
name = att.get("title", "Unknown")
|
|
290
|
+
att_id = att.get("id", "Unknown")
|
|
291
|
+
size = att.get("size", 0)
|
|
292
|
+
mime_type = att.get("metadata", {}).get("mediaType", "unknown")
|
|
293
|
+
|
|
294
|
+
table.add_row(name, att_id, f"{size:,} bytes", mime_type)
|
|
295
|
+
|
|
296
|
+
console.print(table)
|
|
297
|
+
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
console.print(f"[red]Error: {exc}[/red]")
|
|
300
|
+
raise typer.Exit(code=1) from None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
if __name__ == "__main__":
|
|
304
|
+
app()
|
|
305
|
+
|
|
File without changes
|